You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ho...@apache.org on 2017/01/16 00:11:06 UTC

[48/50] [abbrv] lucene-solr:jira/solr-5944: SOLR-5944: Merge branch 'master' into jira/solr-5944

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/027a92a4/solr/core/src/test/org/apache/solr/update/TestInPlaceUpdatesStandalone.java
----------------------------------------------------------------------
diff --cc solr/core/src/test/org/apache/solr/update/TestInPlaceUpdatesStandalone.java
index cd464a7,0000000..f30c827
mode 100644,000000..100644
--- a/solr/core/src/test/org/apache/solr/update/TestInPlaceUpdatesStandalone.java
+++ b/solr/core/src/test/org/apache/solr/update/TestInPlaceUpdatesStandalone.java
@@@ -1,1099 -1,0 +1,1087 @@@
 +
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements.  See the NOTICE file distributed with
 + * this work for additional information regarding copyright ownership.
 + * The ASF licenses this file to You under the Apache License, Version 2.0
 + * (the "License"); you may not use this file except in compliance with
 + * the License.  You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +
 +package org.apache.solr.update;
 +
 +import static org.junit.internal.matchers.StringContains.containsString;
 +import static org.apache.solr.update.UpdateLogTest.buildAddUpdateCommand;
 +
 +import java.util.Arrays;
 +import java.util.Collections;
 +import java.util.HashMap;
 +import java.util.HashSet;
 +import java.util.LinkedHashMap;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.Random;
 +import java.util.Set;
 +
 +import org.apache.lucene.index.FieldInfo;
 +import org.apache.lucene.util.TestUtil;
 +import org.apache.solr.SolrTestCaseJ4;
 +import org.apache.solr.client.solrj.SolrClient;
 +import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
 +import org.apache.solr.common.SolrDocument;
 +import org.apache.solr.common.SolrDocumentList;
 +import org.apache.solr.common.SolrException;
 +import org.apache.solr.common.SolrInputDocument;
 +import org.apache.solr.common.SolrInputField;
 +import org.apache.solr.request.SolrQueryRequest;
 +import org.apache.solr.update.processor.DistributedUpdateProcessor;
- import org.apache.solr.update.processor.DistributedUpdateProcessor.DistribPhase;
 +import org.apache.solr.schema.IndexSchema;
 +import org.apache.solr.schema.SchemaField;
 +import org.apache.solr.search.SolrIndexSearcher;
 +import org.apache.solr.update.processor.AtomicUpdateDocumentMerger;
 +import org.apache.solr.util.RefCounted;
 +import org.junit.Before;
 +import org.junit.BeforeClass;
 +import org.junit.Test;
 +
- import static org.apache.solr.update.processor.DistributingUpdateProcessorFactory.DISTRIB_UPDATE_PARAM;
- 
 +
 +/**
 + * Tests the in-place updates (docValues updates) for a standalone Solr instance.
 + */
 +public class TestInPlaceUpdatesStandalone extends SolrTestCaseJ4 {
 +  private static SolrClient client;
 +
 +  @BeforeClass
 +  public static void beforeClass() throws Exception {
 +    
 +    initCore("solrconfig-tlog.xml", "schema-inplace-updates.xml");
 +
 +    // sanity check that autocommits are disabled
 +    assertEquals(-1, h.getCore().getSolrConfig().getUpdateHandlerInfo().autoCommmitMaxTime);
 +    assertEquals(-1, h.getCore().getSolrConfig().getUpdateHandlerInfo().autoSoftCommmitMaxTime);
 +    assertEquals(-1, h.getCore().getSolrConfig().getUpdateHandlerInfo().autoCommmitMaxDocs);
 +    assertEquals(-1, h.getCore().getSolrConfig().getUpdateHandlerInfo().autoSoftCommmitMaxDocs);
 +
 +    // validate that the schema was not changed to an unexpected state
 +    IndexSchema schema = h.getCore().getLatestSchema();
 +    for (String fieldName : Arrays.asList("_version_",
 +                                          "inplace_l_dvo",
 +                                          "inplace_updatable_float",
 +                                          "inplace_updatable_int", 
 +                                          "inplace_updatable_float_with_default",
 +                                          "inplace_updatable_int_with_default")) {
 +      // these fields must only be using docValues to support inplace updates
 +      SchemaField field = schema.getField(fieldName);
 +      assertTrue(field.toString(),
 +                 field.hasDocValues() && ! field.indexed() && ! field.stored());
 +    }
 +    for (String fieldName : Arrays.asList("title_s", "regular_l", "stored_i")) {
 +      // these fields must support atomic updates, but not inplace updates (ie: stored)
 +      SchemaField field = schema.getField(fieldName);
 +      assertTrue(field.toString(), field.stored());
 +    }    
 +
 +    // Don't close this client, it would shutdown the CoreContainer
 +    client = new EmbeddedSolrServer(h.getCoreContainer(), h.coreName);
 +  }
 +
-   @Override
-   public void clearIndex() {
- 
-   }
-   
 +  @Before
 +  public void deleteAllAndCommit() throws Exception {
-     // workaround for SOLR-9934
-     // we need to ensure that all low level IndexWriter metadata (about docvalue fields) is also deleted
-     deleteByQueryAndGetVersion("*:*", params("_version_", Long.toString(-Long.MAX_VALUE), DISTRIB_UPDATE_PARAM, DistribPhase.FROMLEADER.toString()));
-     // nocommit: if SOLR-9934 is committed before this branch is merged, replace above line with simple call to clearIndex(); 
-     
++    clearIndex();
 +    assertU(commit("softCommit", "false"));
 +  }
 +
 +  @Test
 +  public void testUpdatingDocValues() throws Exception {
 +    long version1 = addAndGetVersion(sdoc("id", "1", "title_s", "first", "inplace_updatable_float", 41), null);
 +    long version2 = addAndGetVersion(sdoc("id", "2", "title_s", "second", "inplace_updatable_float", 42), null);
 +    long version3 = addAndGetVersion(sdoc("id", "3", "title_s", "third", "inplace_updatable_float", 43), null);
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "*:*"), "//*[@numFound='3']");
 +
 +    // the reason we're fetching these docids is to validate that the subsequent updates 
 +    // are done in place and don't cause the docids to change
 +    int docid1 = getDocId("1");
 +    int docid2 = getDocId("2");
 +    int docid3 = getDocId("3");
 +
 +    // Check docValues were "set"
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("set", 200));
 +    version2 = addAndAssertVersion(version2, "id", "2", "inplace_updatable_float", map("set", 300));
 +    version3 = addAndAssertVersion(version3, "id", "3", "inplace_updatable_float", map("set", 100));
 +    assertU(commit("softCommit", "false"));
 +
 +    assertQ(req("q", "*:*", "sort", "id asc", "fl", "*,[docid]"),
 +        "//*[@numFound='3']",
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='200.0']",
 +        "//result/doc[2]/float[@name='inplace_updatable_float'][.='300.0']",
 +        "//result/doc[3]/float[@name='inplace_updatable_float'][.='100.0']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+version1+"']",
 +        "//result/doc[2]/long[@name='_version_'][.='"+version2+"']",
 +        "//result/doc[3]/long[@name='_version_'][.='"+version3+"']",
 +        "//result/doc[1]/int[@name='[docid]'][.='"+docid1+"']",
 +        "//result/doc[2]/int[@name='[docid]'][.='"+docid2+"']",
 +        "//result/doc[3]/int[@name='[docid]'][.='"+docid3+"']"
 +        );
 +
 +    // Check docValues are "inc"ed
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("inc", 1));
 +    version2 = addAndAssertVersion(version2, "id", "2", "inplace_updatable_float", map("inc", -2));
 +    version3 = addAndAssertVersion(version3, "id", "3", "inplace_updatable_float", map("inc", 3));
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "*:*", "sort", "id asc", "fl", "*,[docid]"),
 +        "//*[@numFound='3']",
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='201.0']",
 +        "//result/doc[2]/float[@name='inplace_updatable_float'][.='298.0']",
 +        "//result/doc[3]/float[@name='inplace_updatable_float'][.='103.0']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+version1+"']",
 +        "//result/doc[2]/long[@name='_version_'][.='"+version2+"']",
 +        "//result/doc[3]/long[@name='_version_'][.='"+version3+"']",
 +        "//result/doc[1]/int[@name='[docid]'][.='"+docid1+"']",
 +        "//result/doc[2]/int[@name='[docid]'][.='"+docid2+"']",
 +        "//result/doc[3]/int[@name='[docid]'][.='"+docid3+"']"
 +        );
 +
 +    // Check back to back "inc"s are working (off the transaction log)
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("inc", 1));
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("inc", 2)); // new value should be 204
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "id:1", "fl", "*,[docid]"),
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='204.0']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+version1+"']",
 +        "//result/doc[1]/int[@name='[docid]'][.='"+docid1+"']");
 +
 +    // Now let the document be atomically updated (non-inplace), ensure the old docvalue is part of new doc
 +    version1 = addAndAssertVersion(version1, "id", "1", "title_s", map("set", "new first"));
 +    assertU(commit("softCommit", "false"));
 +    int newDocid1 = getDocId("1");
 +    assertTrue(newDocid1 != docid1);
 +    docid1 = newDocid1;
 +
 +    assertQ(req("q", "id:1"),
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='204.0']",
 +        "//result/doc[1]/str[@name='title_s'][.='new first']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+version1+"']");
 +
 +    // Check if atomic update with "inc" to a docValue works
 +    version2 = addAndAssertVersion(version2, "id", "2", "title_s", map("set", "new second"), "inplace_updatable_float", map("inc", 2));
 +    assertU(commit("softCommit", "false"));
 +    int newDocid2 = getDocId("2");
 +    assertTrue(newDocid2 != docid2);
 +    docid2 = newDocid2;
 +
 +    assertQ(req("q", "id:2"),
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='300.0']",
 +        "//result/doc[1]/str[@name='title_s'][.='new second']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+version2+"']");
 +
 +    // Check if docvalue "inc" update works for a newly created document, which is not yet committed
 +    // Case1: docvalue was supplied during add of new document
 +    long version4 = addAndGetVersion(sdoc("id", "4", "title_s", "fourth", "inplace_updatable_float", "400"), params());
 +    version4 = addAndAssertVersion(version4, "id", "4", "inplace_updatable_float", map("inc", 1));
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "id:4"),
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='401.0']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+version4+"']");
 +
 +    // Check if docvalue "inc" update works for a newly created document, which is not yet committed
 +    // Case2: docvalue was not supplied during add of new document, should assume default
 +    long version5 = addAndGetVersion(sdoc("id", "5", "title_s", "fifth"), params());
 +    version5 = addAndAssertVersion(version5, "id", "5", "inplace_updatable_float", map("inc", 1));
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "id:5"),
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='1.0']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+version5+"']");
 +
 +    // Check if docvalue "set" update works for a newly created document, which is not yet committed
 +    long version6 = addAndGetVersion(sdoc("id", "6", "title_s", "sixth"), params());
 +    version6 = addAndAssertVersion(version6, "id", "6", "inplace_updatable_float", map("set", 600));
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "id:6"),
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='600.0']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+version6+"']");
 +
 +    // Check optimistic concurrency works
 +    long v20 = addAndGetVersion(sdoc("id", "20", "title_s","first", "inplace_updatable_float", 100), params());    
 +    SolrException exception = expectThrows(SolrException.class, () -> {
 +      addAndGetVersion(sdoc("id","20", "_version_", -1, "inplace_updatable_float", map("inc", 1)), null);
 +    });
 +    assertEquals(exception.toString(), SolrException.ErrorCode.CONFLICT.code, exception.code());
 +    assertThat(exception.getMessage(), containsString("expected=-1"));
 +    assertThat(exception.getMessage(), containsString("actual="+v20));
 +
 +
 +    long oldV20 = v20;
 +    v20 = addAndAssertVersion(v20, "id","20", "_version_", v20, "inplace_updatable_float", map("inc", 1));
 +    exception = expectThrows(SolrException.class, () -> {
 +      addAndGetVersion(sdoc("id","20", "_version_", oldV20, "inplace_updatable_float", map("inc", 1)), null);
 +    });
 +    assertEquals(exception.toString(), SolrException.ErrorCode.CONFLICT.code, exception.code());
 +    assertThat(exception.getMessage(), containsString("expected="+oldV20));
 +    assertThat(exception.getMessage(), containsString("actual="+v20));
 +
 +    v20 = addAndAssertVersion(v20, "id","20", "_version_", v20, "inplace_updatable_float", map("inc", 1));
 +    // RTG before a commit
 +    assertJQ(req("qt","/get", "id","20", "fl","id,inplace_updatable_float,_version_"),
 +        "=={'doc':{'id':'20', 'inplace_updatable_float':" + 102.0 + ",'_version_':" + v20 + "}}");
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "id:20"), 
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='102.0']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+v20+"']");
 +
 +    // Check if updated DVs can be used for search
 +    assertQ(req("q", "inplace_updatable_float:102"), 
 +        "//result/doc[1]/str[@name='id'][.='20']",
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='102.0']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+v20+"']");
 +
 +    // Check if updated DVs can be used for sorting
 +    assertQ(req("q", "*:*", "sort", "inplace_updatable_float asc"), 
 +        "//result/doc[4]/str[@name='id'][.='1']",
 +        "//result/doc[4]/float[@name='inplace_updatable_float'][.='204.0']",
 +
 +        "//result/doc[5]/str[@name='id'][.='2']",
 +        "//result/doc[5]/float[@name='inplace_updatable_float'][.='300.0']",
 +
 +        "//result/doc[3]/str[@name='id'][.='3']",
 +        "//result/doc[3]/float[@name='inplace_updatable_float'][.='103.0']",
 +
 +        "//result/doc[6]/str[@name='id'][.='4']",
 +        "//result/doc[6]/float[@name='inplace_updatable_float'][.='401.0']",
 +
 +        "//result/doc[1]/str[@name='id'][.='5']",
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='1.0']",
 +
 +        "//result/doc[7]/str[@name='id'][.='6']",
 +        "//result/doc[7]/float[@name='inplace_updatable_float'][.='600.0']",
 +
 +        "//result/doc[2]/str[@name='id'][.='20']",
 +        "//result/doc[2]/float[@name='inplace_updatable_float'][.='102.0']");
 +  }
 +
 +  @Test
 +  public void testUpdateTwoDifferentFields() throws Exception {
 +    long version1 = addAndGetVersion(sdoc("id", "1", "title_s", "first", "inplace_updatable_float", 42), null);
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "*:*"), "//*[@numFound='1']");
 +
 +    int docid1 = getDocId("1");
 +
 +    // Check docValues were "set"
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("set", 200));
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_int", map("set", 10));
 +    assertU(commit("softCommit", "false"));
 +
 +    assertU(commit("softCommit", "false"));
 +
 +    assertQ(req("q", "*:*", "sort", "id asc", "fl", "*,[docid]"),
 +        "//*[@numFound='1']",
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='200.0']",
 +        "//result/doc[1]/long[@name='_version_'][.='"+version1+"']",
 +        "//result/doc[1]/int[@name='[docid]'][.='"+docid1+"']"
 +        );
 +
 +    // two different update commands, updating each of the fields separately
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_int", map("inc", 1));
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("inc", 1));
 +    // same update command, updating both the fields together
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_int", map("inc", 1),
 +        "inplace_updatable_float", map("inc", 1));
 +
 +    if (random().nextBoolean()) {
 +      assertU(commit("softCommit", "false"));
 +      assertQ(req("q", "*:*", "sort", "id asc", "fl", "*,[docid]"),
 +          "//*[@numFound='1']",
 +          "//result/doc[1]/float[@name='inplace_updatable_float'][.='202.0']",
 +          "//result/doc[1]/int[@name='inplace_updatable_int'][.='12']",
 +          "//result/doc[1]/long[@name='_version_'][.='"+version1+"']",
 +          "//result/doc[1]/int[@name='[docid]'][.='"+docid1+"']"
 +          );
 +    } 
 +
 +    // RTG
 +    assertJQ(req("qt","/get", "id","1", "fl","id,inplace_updatable_float,inplace_updatable_int"),
 +        "=={'doc':{'id':'1', 'inplace_updatable_float':" + 202.0 + ",'inplace_updatable_int':" + 12 + "}}");
 +
 +  }
 +
 +  @Test
 +  public void testDVUpdatesWithDBQofUpdatedValue() throws Exception {
 +    long version1 = addAndGetVersion(sdoc("id", "1", "title_s", "first", "inplace_updatable_float", "0"), null);
 +    assertU(commit());
 +
 +    // in-place update
 +    addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("set", 100), "_version_", version1);
 +
 +    // DBQ where q=inplace_updatable_float:100
 +    assertU(delQ("inplace_updatable_float:100"));
 +
 +    assertU(commit());
 +
 +    assertQ(req("q", "*:*"), "//*[@numFound='0']");
 +  }
 +
 +  @Test
 +  public void testDVUpdatesWithDelete() throws Exception {
 +    long version1 = 0;
 +
 +    for (boolean postAddCommit : Arrays.asList(true, false)) {
 +      for (boolean delById : Arrays.asList(true, false)) {
 +        for (boolean postDelCommit : Arrays.asList(true, false)) {
 +          addAndGetVersion(sdoc("id", "1", "title_s", "first"), params());
 +          if (postAddCommit) assertU(commit());
 +          assertU(delById ? delI("1") : delQ("id:1"));
 +          if (postDelCommit) assertU(commit());
 +          version1 = addAndGetVersion(sdoc("id", "1", "inplace_updatable_float", map("set", 200)), params());
 +          // assert current doc#1 doesn't have old value of "title_s"
 +          assertU(commit());
 +          assertQ(req("q", "title_s:first", "sort", "id asc", "fl", "*,[docid]"),
 +              "//*[@numFound='0']");
 +        }
 +      }
 +    }
 +
 +    // Update to recently deleted (or non-existent) document with a "set" on updatable 
 +    // field should succeed, since it is executed internally as a full update
 +    // because AUDM.doInPlaceUpdateMerge() returns false
 +    assertU(random().nextBoolean()? delI("1"): delQ("id:1"));
 +    if (random().nextBoolean()) assertU(commit());
 +    addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("set", 200));
 +    assertU(commit());
 +    assertQ(req("q", "id:1", "sort", "id asc", "fl", "*"),
 +        "//*[@numFound='1']",
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='200.0']");
 +
 +    // Another "set" on the same field should be an in-place update 
 +    int docid1 = getDocId("1");
 +    addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("set", 300));
 +    assertU(commit());
 +    assertQ(req("q", "id:1", "fl", "*,[docid]"),
 +        "//result/doc[1]/float[@name='inplace_updatable_float'][.='300.0']",
 +        "//result/doc[1]/int[@name='[docid]'][.='"+docid1+"']");
 +  }
 +
 +  public static long addAndAssertVersion(long expectedCurrentVersion, Object... fields) throws Exception {
 +    assert 0 < expectedCurrentVersion;
 +    long currentVersion = addAndGetVersion(sdoc(fields), null);
 +    assertTrue(currentVersion > expectedCurrentVersion);
 +    return currentVersion;
 +  }
 +
 +  /**
 +   * Helper method to search for the specified (uniqueKey field) id using <code>fl=[docid]</code> 
 +   * and return the internal lucene docid.
 +   */
 +  private int getDocId(String id) throws NumberFormatException, Exception {
 +    SolrDocumentList results = client.query(params("q","id:" + id, "fl", "[docid]")).getResults();
 +    assertEquals(1, results.getNumFound());
 +    assertEquals(1, results.size());
 +    Object docid = results.get(0).getFieldValue("[docid]");
 +    assertTrue(docid instanceof Integer);
 +    return ((Integer)docid);
 +  }
 +
 +  @Test
 +  public void testUpdateOfNonExistentDVsShouldNotFail() throws Exception {
 +    // schema sanity check: assert that the nonexistent_field_i_dvo doesn't exist already
 +    FieldInfo fi;
 +    RefCounted<SolrIndexSearcher> holder = h.getCore().getSearcher();
 +    try {
 +      fi = holder.get().getSlowAtomicReader().getFieldInfos().fieldInfo("nonexistent_field_i_dvo");
 +    } finally {
 +      holder.decref();
 +    }
 +    assertNull(fi);
 +
 +    // Partial update
 +    addAndGetVersion(sdoc("id", "0", "nonexistent_field_i_dvo", map("set", "42")), null);
 +
 +    addAndGetVersion(sdoc("id", "1"), null);
 +    addAndGetVersion(sdoc("id", "1", "nonexistent_field_i_dvo", map("inc", "1")), null);
 +    addAndGetVersion(sdoc("id", "1", "nonexistent_field_i_dvo", map("inc", "1")), null);
 +
 +    assertU(commit());
 +
 +    assertQ(req("q", "*:*"), "//*[@numFound='2']");    
 +    assertQ(req("q", "nonexistent_field_i_dvo:42"), "//*[@numFound='1']");    
 +    assertQ(req("q", "nonexistent_field_i_dvo:2"), "//*[@numFound='1']");    
 +  }
 +
 +  @Test
 +  public void testOnlyPartialUpdatesBetweenCommits() throws Exception {
 +    // Full updates
 +    long version1 = addAndGetVersion(sdoc("id", "1", "title_s", "first", "val1_i_dvo", "1", "val2_l_dvo", "1"), params());
 +    long version2 = addAndGetVersion(sdoc("id", "2", "title_s", "second", "val1_i_dvo", "2", "val2_l_dvo", "2"), params());
 +    long version3 = addAndGetVersion(sdoc("id", "3", "title_s", "third", "val1_i_dvo", "3", "val2_l_dvo", "3"), params());
 +    assertU(commit("softCommit", "false"));
 +
 +    assertQ(req("q", "*:*", "fl", "*,[docid]"), "//*[@numFound='3']");
 +
 +    int docid1 = getDocId("1");
 +    int docid2 = getDocId("2");
 +    int docid3 = getDocId("3");
 +
 +    int numPartialUpdates = 1 + random().nextInt(5000);
 +    for (int i=0; i<numPartialUpdates; i++) {
 +      version1 = addAndAssertVersion(version1, "id", "1", "val1_i_dvo", map("set", i));
 +      version2 = addAndAssertVersion(version2, "id", "2", "val1_i_dvo", map("inc", 1));
 +      version3 = addAndAssertVersion(version3, "id", "3", "val1_i_dvo", map("set", i));
 +
 +      version1 = addAndAssertVersion(version1, "id", "1", "val2_l_dvo", map("set", i));
 +      version2 = addAndAssertVersion(version2, "id", "2", "val2_l_dvo", map("inc", 1));
 +      version3 = addAndAssertVersion(version3, "id", "3", "val2_l_dvo", map("set", i));
 +    }
 +    assertU(commit("softCommit", "true"));
 +
 +    assertQ(req("q", "*:*", "sort", "id asc", "fl", "*,[docid]"),
 +        "//*[@numFound='3']",
 +        "//result/doc[1]/int[@name='val1_i_dvo'][.='"+(numPartialUpdates-1)+"']",
 +        "//result/doc[2]/int[@name='val1_i_dvo'][.='"+(numPartialUpdates+2)+"']",
 +        "//result/doc[3]/int[@name='val1_i_dvo'][.='"+(numPartialUpdates-1)+"']",
 +        "//result/doc[1]/long[@name='val2_l_dvo'][.='"+(numPartialUpdates-1)+"']",
 +        "//result/doc[2]/long[@name='val2_l_dvo'][.='"+(numPartialUpdates+2)+"']",
 +        "//result/doc[3]/long[@name='val2_l_dvo'][.='"+(numPartialUpdates-1)+"']",
 +        "//result/doc[1]/int[@name='[docid]'][.='"+docid1+"']",
 +        "//result/doc[2]/int[@name='[docid]'][.='"+docid2+"']",
 +        "//result/doc[3]/int[@name='[docid]'][.='"+docid3+"']",
 +        "//result/doc[1]/long[@name='_version_'][.='" + version1 + "']",
 +        "//result/doc[2]/long[@name='_version_'][.='" + version2 + "']",
 +        "//result/doc[3]/long[@name='_version_'][.='" + version3 + "']"
 +        );
 +  }
 +
 +  /**
 +   * Useful to store the state of an expected document into an in-memory model
 +   * representing the index.
 +   */
 +  private static class DocInfo {
 +    public final long version;
 +    public final Long value;
 +
 +    public DocInfo(long version, Long val) {
 +      this.version = version;
 +      this.value = val;
 +    }
 +
 +    @Override
 +    public String toString() {
 +      return "["+version+", "+value+"]";
 +    }
 +  }
 +
 +  /** @see #checkReplay */
 +  @Test
 +  public void testReplay_AfterInitialAddMixOfIncAndSet() throws Exception {
 +    checkReplay("val2_l_dvo",
 +        //
 +        sdoc("id", "0", "val2_l_dvo", 3000000000L),
 +        sdoc("id", "0", "val2_l_dvo", map("inc", 3)),
 +        HARDCOMMIT,
 +        sdoc("id", "0", "val2_l_dvo", map("inc", 5)),
 +        sdoc("id", "1", "val2_l_dvo", 2000000000L),
 +        sdoc("id", "1", "val2_l_dvo", map("set", 2000000002L)),
 +        sdoc("id", "1", "val2_l_dvo", map("set", 3000000000L)),
 +        sdoc("id", "0", "val2_l_dvo", map("inc", 7)),
 +        sdoc("id", "1", "val2_l_dvo", map("set", 7000000000L)),
 +        sdoc("id", "0", "val2_l_dvo", map("inc", 11)),
 +        sdoc("id", "2", "val2_l_dvo", 2000000000L),
 +        HARDCOMMIT,
 +        sdoc("id", "2", "val2_l_dvo", map("set", 3000000000L)),
 +        HARDCOMMIT);
 +  }
 +
 +  /** @see #checkReplay */
 +  @Test
 +  public void testReplay_AfterInitialAddMixOfIncAndSetAndFullUpdates() throws Exception {
 +    checkReplay("val2_l_dvo",
 +        //
 +        sdoc("id", "0", "val2_l_dvo", 3000000000L),
 +        sdoc("id", "0", "val2_l_dvo", map("set", 3000000003L)),
 +        HARDCOMMIT,
 +        sdoc("id", "0", "val2_l_dvo", map("set", 3000000008L)),
 +        sdoc("id", "1", "val2_l_dvo", 2000000000L),
 +        sdoc("id", "1", "val2_l_dvo", map("inc", 2)),
 +        sdoc("id", "1", "val2_l_dvo", 3000000000L),
 +        sdoc("id", "0", "val2_l_dvo", map("set", 3000000015L)),
 +        sdoc("id", "1", "val2_l_dvo", 7000000000L),
 +        sdoc("id", "0", "val2_l_dvo", map("set", 3000000026L)),
 +        sdoc("id", "2", "val2_l_dvo", 2000000000L),
 +        HARDCOMMIT,
 +        sdoc("id", "2", "val2_l_dvo", 3000000000L),
 +        HARDCOMMIT);
 +  }
 +
 +  /** @see #checkReplay */
 +  @Test
 +  public void testReplay_AllUpdatesAfterInitialAddAreInc() throws Exception {
 +    checkReplay("val2_l_dvo",
 +        //
 +        sdoc("id", "0", "val2_l_dvo", 3000000000L),
 +        sdoc("id", "0", "val2_l_dvo", map("inc", 3)),
 +        HARDCOMMIT,
 +        sdoc("id", "0", "val2_l_dvo", map("inc", 5)),
 +        sdoc("id", "1", "val2_l_dvo", 2000000000L),
 +        sdoc("id", "1", "val2_l_dvo", map("inc", 2)),
 +        sdoc("id", "1", "val2_l_dvo", 3000000000L),
 +        sdoc("id", "0", "val2_l_dvo", map("inc", 7)),
 +        sdoc("id", "1", "val2_l_dvo", 7000000000L),
 +        sdoc("id", "0", "val2_l_dvo", map("inc", 11)),
 +        sdoc("id", "2", "val2_l_dvo", 2000000000L),
 +        HARDCOMMIT,
 +        sdoc("id", "2", "val2_l_dvo", 3000000000L),
 +        HARDCOMMIT);
 +  }
 +
 +  /** @see #checkReplay */
 +  @Test
 +  public void testReplay_AllUpdatesAfterInitialAddAreSets() throws Exception {
 +    checkReplay("val2_l_dvo",
 +        //
 +        sdoc("id", "0", "val2_l_dvo", 3000000000L),
 +        sdoc("id", "0", "val2_l_dvo", map("set", 3000000003L)),
 +        HARDCOMMIT,
 +        sdoc("id", "0", "val2_l_dvo", map("set", 3000000008L)),
 +        sdoc("id", "1", "val2_l_dvo", 2000000000L),
 +        sdoc("id", "1", "val2_l_dvo", map("set", 2000000002L)),
 +        sdoc("id", "1", "val2_l_dvo", map("set", 3000000000L)),
 +        sdoc("id", "0", "val2_l_dvo", map("set", 3000000015L)),
 +        sdoc("id", "1", "val2_l_dvo", map("set", 7000000000L)),
 +        sdoc("id", "0", "val2_l_dvo", map("set", 3000000026L)),
 +        sdoc("id", "2", "val2_l_dvo", 2000000000L),
 +        HARDCOMMIT,
 +        sdoc("id", "2", "val2_l_dvo", map("set", 3000000000L)),
 +        HARDCOMMIT
 +        );
 +  }
 +  
 +  /** @see #checkReplay */
 +  @Test
 +  public void testReplay_MixOfInplaceAndNonInPlaceAtomicUpdates() throws Exception {
 +    checkReplay("inplace_l_dvo",
 +                //
 +                sdoc("id", "3", "inplace_l_dvo", map("inc", -13)),
 +                sdoc("id", "3", "inplace_l_dvo", map("inc", 19),    "regular_l", map("inc", -17)),
 +                sdoc("id", "1",                                     "regular_l", map("inc", -19)),
 +                sdoc("id", "3", "inplace_l_dvo", map("inc", -11)),
 +                sdoc("id", "2", "inplace_l_dvo", map("set", 28)),
 +                HARDCOMMIT,
 +                sdoc("id", "2", "inplace_l_dvo", map("inc", 45)),
 +                sdoc("id", "3", "inplace_l_dvo", map("set", 72)),
 +                sdoc("id", "2",                                     "regular_l", map("inc", -55)),
 +                sdoc("id", "2", "inplace_l_dvo", -48,               "regular_l", 159),
 +                sdoc("id", "3", "inplace_l_dvo", 52,                "regular_l", 895),
 +                sdoc("id", "2", "inplace_l_dvo", map("inc", 19)),
 +                sdoc("id", "3", "inplace_l_dvo", map("inc", -264),  "regular_l", map("inc", -207)),
 +                sdoc("id", "3", "inplace_l_dvo", -762,              "regular_l", 272),
 +                SOFTCOMMIT);
 +  }
 +  
 +  @Test
 +  public void testReplay_SetOverriddenWithNoValueThenInc() throws Exception {
 +    final String inplaceField = "inplace_l_dvo"; 
 +    checkReplay(inplaceField,
 +                //
 +                sdoc("id", "1", inplaceField, map("set", 555L)),
 +                SOFTCOMMIT,
 +                sdoc("id", "1", "regular_l", 666L), // NOTE: no inplaceField, regular add w/overwrite 
 +                sdoc("id", "1", inplaceField, map("inc", -77)),
 +                HARDCOMMIT);
 +  }
 +
 +  /** 
 +   * Simple enum for randomizing a type of update.
 +   * Each enum value has an associated probability, and the class has built in sanity checks 
 +   * that the total is 100%
 +   * 
 +   * @see RandomUpdate#pick
 +   * @see #checkRandomReplay
 +   */
 +  private static enum RandomUpdate {
 +    HARD_COMMIT(5), 
 +    SOFT_COMMIT(5),
 +
 +    /** doc w/o the inplaceField, atomic update on some other (non-inplace) field */
 +    ATOMIC_NOT_INPLACE(5),
 +    
 +    /** atomic update of a doc w/ inc on both inplaceField *AND* non-inplace field */
 +    ATOMIC_INPLACE_AND_NOT_INPLACE(10), 
 +
 +    
 +    /** atomic update of a doc w/ set inplaceField */
 +    ATOMIC_INPLACE_SET(25),
 +    /** atomic update of a doc w/ inc inplaceField */
 +    ATOMIC_INPLACE_INC(25), 
 +    
 +    /** doc w/o the inplaceField, normal add */
 +    ADD_NO_INPLACE_VALUE(5),
 +    /** a non atomic update of a doc w/ new inplaceField value */
 +    ADD_INPLACE_VALUE(20); 
 +    
 +    private RandomUpdate(int odds) {
 +      this.odds = odds;
 +    }
 +    public final int odds;
 +
 +    static { // sanity check odds add up to 100%
 +      int total = 0;
 +      for (RandomUpdate candidate : RandomUpdate.values()) {
 +        total += candidate.odds;
 +      }
 +      assertEquals("total odds doesn't equal 100", 100, total);
 +    }
 +
 +    /** pick a random type of RandomUpdate */
 +    public static final RandomUpdate pick(Random r) {
 +      final int target = TestUtil.nextInt(r, 1, 100);
 +      int cumulative_odds = 0;
 +      for (RandomUpdate candidate : RandomUpdate.values()) {
 +        cumulative_odds += candidate.odds;
 +        if (target <= cumulative_odds) {
 +          return candidate;
 +        }
 +      }
 +      fail("how did we not find a candidate? target=" + target + ", cumulative_odds=" + cumulative_odds);
 +      return null; // compiler mandated return
 +    }
 +  }
 +  
 +  /** @see #checkRandomReplay */
 +  @Test
 +  public void testReplay_Random_ManyDocsManyUpdates() throws Exception {
 +    
 +    // build up a random list of updates
 +    final int maxDocId = atLeast(50);
 +    final int numUpdates = maxDocId * 3;
 +    checkRandomReplay(maxDocId, numUpdates);
 +  }
 +  
 +  /** @see #checkRandomReplay */
 +  @Test
 +  public void testReplay_Random_FewDocsManyUpdates() throws Exception {
 +    
 +    // build up a random list of updates
 +    final int maxDocId = atLeast(3);
 +    final int numUpdates = maxDocId * 50;
 +    checkRandomReplay(maxDocId, numUpdates);
 +  }
 +  
 +  /** @see #checkRandomReplay */
 +  @Test
 +  public void testReplay_Random_FewDocsManyShortSequences() throws Exception {
 +    
 +    // build up a random list of updates
 +    final int numIters = atLeast(50);
 +    
 +    for (int i = 0; i < numIters; i++) {
 +      final int maxDocId = atLeast(3);
 +      final int numUpdates = maxDocId * 5;
 +      checkRandomReplay(maxDocId, numUpdates);
 +      deleteAllAndCommit();
 +    }
 +  }
 +
 +
 +  /** 
 +   * @see #checkReplay 
 +   * @see RandomUpdate
 +   */
 +  public void checkRandomReplay(final int maxDocId, final int numCmds) throws Exception {
 +    
 +    final String not_inplaceField = "regular_l";
 +    final String inplaceField = "inplace_l_dvo"; 
 +
 +    final Object[] cmds = new Object[numCmds];
 +    for (int iter = 0; iter < numCmds; iter++) {
 +      final int id = TestUtil.nextInt(random(), 1, maxDocId);
 +      final RandomUpdate update = RandomUpdate.pick(random());
 +
 +      switch (update) {
 +        
 +      case HARD_COMMIT:
 +        cmds[iter] = HARDCOMMIT;
 +        break;
 +        
 +      case SOFT_COMMIT:
 +        cmds[iter] = SOFTCOMMIT;
 +        break;
 +
 +      case ATOMIC_NOT_INPLACE:
 +        // atomic update on non_inplaceField, w/o any value specified for inplaceField
 +        cmds[iter] = sdoc("id", id,
 +                          not_inplaceField, map("inc", random().nextInt()));
 +        break;
 +        
 +      case ATOMIC_INPLACE_AND_NOT_INPLACE:
 +        // atomic update of a doc w/ inc on both inplaceField and not_inplaceField
 +        cmds[iter] = sdoc("id", id,
 +                          inplaceField, map("inc", random().nextInt()),
 +                          not_inplaceField, map("inc", random().nextInt()));
 +        break;
 +
 +      case ATOMIC_INPLACE_SET:
 +        // atomic update of a doc w/ set inplaceField
 +        cmds[iter] = sdoc("id", id,
 +                          inplaceField, map("set", random().nextLong()));
 +        break;
 +
 +      case ATOMIC_INPLACE_INC:
 +        // atomic update of a doc w/ inc inplaceField
 +        cmds[iter] = sdoc("id", id,
 +                          inplaceField, map("inc", random().nextInt()));
 +        break;
 +
 +      case ADD_NO_INPLACE_VALUE:
 +        // regular add of doc w/o the inplaceField, but does include non_inplaceField
 +        cmds[iter] = sdoc("id", id,
 +                          not_inplaceField, random().nextLong());
 +        break;
 +
 +      case ADD_INPLACE_VALUE:
 +        // a non atomic update of a doc w/ new inplaceField value
 +        cmds[iter] = sdoc("id", id,
 +                          inplaceField, random().nextLong(),
 +                          not_inplaceField, random().nextLong());
 +        break;
 +        
 +      default:
 +        fail("WTF is this? ... " + update);
 +      }
 +      
 +      assertNotNull(cmds[iter]); // sanity check switch
 +    }
 +
 +    checkReplay(inplaceField, cmds);
 +  }
 +  
 +  /** sentinal object for {@link #checkReplay} */
 +  public Object SOFTCOMMIT = new Object() { public String toString() { return "SOFTCOMMIT"; } };
 +  /** sentinal object for {@link #checkReplay} */
 +  public Object HARDCOMMIT = new Object() { public String toString() { return "HARDCOMMIT"; } };
 +
 +  /**
 +   * Executes a sequence of commands against Solr, while tracking the expected value of a specified 
 +   * <code>valField</code> Long field (presumably that only uses docvalues) against an in memory model 
 +   * maintained in parallel (for the purpose of testing the correctness of in-place updates..
 +   *
 +   * <p>
 +   * A few restrictions are placed on the {@link SolrInputDocument}s that can be included when using 
 +   * this method, in order to keep the in-memory model management simple:
 +   * </p>
 +   * <ul>
 +   *  <li><code>id</code> must be uniqueKey field</li>
 +   *  <li><code>id</code> may have any FieldType, but all values must be parsable as Integers</li>
 +   *  <li><code>valField</code> must be a single valued field</li>
 +   *  <li>All values in the <code>valField</code> must either be {@link Number}s, or Maps containing 
 +   *      atomic updates ("inc" or "set") where the atomic value is a {@link Number}</li>
 +   * </ul>
 +   * 
 +   * @param valField the field to model
 +   * @param commands A sequence of Commands which can either be SolrInputDocuments 
 +   *                 (regular or containing atomic update Maps)
 +   *                 or one of the {@link TestInPlaceUpdatesStandalone#HARDCOMMIT} or {@link TestInPlaceUpdatesStandalone#SOFTCOMMIT} sentinal objects.
 +   */
 +  public void checkReplay(final String valField, Object... commands) throws Exception {
 +    
 +    HashMap<Integer, DocInfo> model = new LinkedHashMap<>();
 +    HashMap<Integer, DocInfo> committedModel = new LinkedHashMap<>();
 +
 +    // by default, we only check the committed model after a commit
 +    // of if the number of total commands is relatively small.
 +    //
 +    // (in theory, there's no reason to check the committed model unless we know there's been a commit
 +    // but for smaller tests the overhead of doing so is tiny, so we might as well)
 +    //
 +    // if some test seed fails, and you want to force the committed model to be checked
 +    // after every command, just temporaribly force this variable to true...
 +    boolean checkCommittedModel = (commands.length < 50);
 +    
 +    for (Object cmd : commands) {
 +      if (cmd == SOFTCOMMIT) {
 +        assertU(commit("softCommit", "true"));
 +        committedModel = new LinkedHashMap(model);
 +        checkCommittedModel = true;
 +      } else if (cmd == HARDCOMMIT) {
 +        assertU(commit("softCommit", "false"));
 +        committedModel = new LinkedHashMap(model);
 +        checkCommittedModel = true;
 +      } else {
 +        assertNotNull("null command in checkReplay", cmd);
 +        assertTrue("cmd is neither sentinal (HARD|SOFT)COMMIT object, nor Solr doc: " + cmd.getClass(),
 +                   cmd instanceof SolrInputDocument);
 +        
 +        final SolrInputDocument sdoc = (SolrInputDocument) cmd;
 +        final int id = Integer.parseInt(sdoc.getFieldValue("id").toString());
 +        
 +        final DocInfo previousInfo = model.get(id);
 +        final Long previousValue = (null == previousInfo) ? null : previousInfo.value;
 +        
 +        final long version = addAndGetVersion(sdoc, null);
 +        
 +        final Object val = sdoc.getFieldValue(valField);
 +        if (val instanceof Map) {
 +          // atomic update of the field we're modeling
 +          
 +          Map<String,?> atomicUpdate = (Map) val;
 +          assertEquals(sdoc.toString(), 1, atomicUpdate.size());
 +          if (atomicUpdate.containsKey("inc")) {
 +            // Solr treats inc on a non-existing doc (or doc w/o existing value) as if existing value is 0
 +            final long base = (null == previousValue) ? 0L : previousValue;
 +            model.put(id, new DocInfo(version,
 +                                      base + ((Number)atomicUpdate.get("inc")).longValue()));
 +          } else if (atomicUpdate.containsKey("set")) {
 +            model.put(id, new DocInfo(version, ((Number)atomicUpdate.get("set")).longValue()));
 +          } else {
 +            fail("wtf update is this? ... " + sdoc);
 +          }
 +        } else if (null == val) {
 +          // the field we are modeling is not mentioned in this update, It's either...
 +          //
 +          // a) a regular update of some other fields (our model should have a null value)
 +          // b) an atomic update of some other field (keep existing value in model)
 +          //
 +          // for now, assume it's atomic and we're going to keep our existing value...
 +          Long newValue = (null == previousInfo) ? null : previousInfo.value;
 +          for (SolrInputField field : sdoc) {
 +            if (! ( "id".equals(field.getName()) || (field.getValue() instanceof Map)) ) {
 +              // not an atomic update, newValue in model should be null
 +              newValue = null;
 +              break;
 +            }
 +          }
 +          model.put(id, new DocInfo(version, newValue));
 +          
 +        } else {
 +          // regular replacement of the value in the field we're modeling
 +          
 +          assertTrue("Model field value is not a Number: " + val.getClass(), val instanceof Number);
 +          model.put(id, new DocInfo(version, ((Number)val).longValue()));
 +        }
 +      }
 +
 +      // after every op, check the model(s)
 +      
 +      // RTG to check the values for every id against the model
 +      for (Map.Entry<Integer, DocInfo> entry : model.entrySet()) {
 +        final Long expected = entry.getValue().value;
 +        assertEquals(expected, client.getById(String.valueOf(entry.getKey())).getFirstValue(valField));
 +      }
 +
 +      // search to check the values for every id in the committed model
 +      if (checkCommittedModel) {
 +        final int numCommitedDocs = committedModel.size();
 +        String[] xpaths = new String[1 + numCommitedDocs];
 +        int i = 0;
 +        for (Map.Entry<Integer, DocInfo> entry : committedModel.entrySet()) {
 +          Integer id = entry.getKey();
 +          Long expected = entry.getValue().value;
 +          if (null != expected) {
 +            xpaths[i] = "//result/doc[./str='"+id+"'][./long='"+expected+"']";
 +          } else {
 +            xpaths[i] = "//result/doc[./str='"+id+"'][not(./long)]";
 +          }           
 +          i++;
 +        }
 +        xpaths[i] = "//*[@numFound='"+numCommitedDocs+"']";
 +        assertQ(req("q", "*:*",
 +                    "fl", "id," + valField,
 +                    "rows", ""+numCommitedDocs),
 +                xpaths);
 +      }
 +    }
 +  }
 +
 +  @Test
 +  public void testMixedInPlaceAndNonInPlaceAtomicUpdates() throws Exception {
 +    SolrDocument rtgDoc = null;
 +    long version1 = addAndGetVersion(sdoc("id", "1", "inplace_updatable_float", "100", "stored_i", "100"), params());
 +
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("inc", "1"), "stored_i", map("inc", "1"));
 +    rtgDoc = client.getById("1");
 +    assertEquals(101, rtgDoc.getFieldValue("stored_i"));
 +    assertEquals(101.0f, rtgDoc.getFieldValue("inplace_updatable_float"));
 +    
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("inc", "1"));
 +    rtgDoc = client.getById("1");
 +    assertEquals(101, rtgDoc.getFieldValue("stored_i"));
 +    assertEquals(102.0f, rtgDoc.getFieldValue("inplace_updatable_float"));
 +
 +    version1 = addAndAssertVersion(version1, "id", "1", "stored_i", map("inc", "1"));
 +    rtgDoc = client.getById("1");
 +    assertEquals(102, rtgDoc.getFieldValue("stored_i"));
 +    assertEquals(102.0f, rtgDoc.getFieldValue("inplace_updatable_float"));
 +
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "*:*", "sort", "id asc", "fl", "*"),
 +            "//*[@numFound='1']",
 +            "//result/doc[1]/float[@name='inplace_updatable_float'][.='102.0']",
 +            "//result/doc[1]/int[@name='stored_i'][.='102']",
 +            "//result/doc[1]/long[@name='_version_'][.='" + version1 + "']"
 +            );
 +
 +    // recheck RTG after commit
 +    rtgDoc = client.getById("1");
 +    assertEquals(102, rtgDoc.getFieldValue("stored_i"));
 +    assertEquals(102.0f, rtgDoc.getFieldValue("inplace_updatable_float"));
 +  }
 +
 +  /** 
 +   * @see #callComputeInPlaceUpdatableFields
 +   * @see AtomicUpdateDocumentMerger#computeInPlaceUpdatableFields 
 +   */
 +  @Test
 +  public void testComputeInPlaceUpdatableFields() throws Exception {
 +    Set<String> inPlaceUpdatedFields = new HashSet<String>();
 +
 +    // these asserts should hold true regardless of type, or wether the field has a default
 +    List<String> fieldsToCheck = Arrays.asList("inplace_updatable_float",
 +                                               "inplace_updatable_int",
 +                                               "inplace_updatable_float_with_default",
 +                                               "inplace_updatable_int_with_default");
 +    Collections.shuffle(fieldsToCheck, random()); // ... and regardless of order checked
 +    for (String field : fieldsToCheck) {
 +      // In-place updatable field updated before it exists SHOULD NOT BE in-place updated:
 +      inPlaceUpdatedFields = callComputeInPlaceUpdatableFields(sdoc("id", "1", "_version_", 42L,
 +                                                                    field, map("set", 10)));
 +      assertFalse(field, inPlaceUpdatedFields.contains(field));
 +      
 +      // In-place updatable field updated after it exists SHOULD BE in-place updated:
 +      addAndGetVersion(sdoc("id", "1", field, "0"), params()); // setting up the dv
 +      inPlaceUpdatedFields = callComputeInPlaceUpdatableFields(sdoc("id", "1", "_version_", 42L,
 +                                                                    field, map("set", 10)));
 +      assertTrue(field, inPlaceUpdatedFields.contains(field));
 +
 +      inPlaceUpdatedFields = callComputeInPlaceUpdatableFields(sdoc("id", "1", "_version_", 42L,
 +                                                                    field, map("inc", 10)));
 +      assertTrue(field, inPlaceUpdatedFields.contains(field));
 +
 +      final String altFieldWithDefault = field.contains("float") ?
 +        "inplace_updatable_int_with_default" : "inplace_updatable_int_with_default";
 +      
 +      // Updating an in-place updatable field (with a default) for the first time.
 +      // DV for it should have been already created when first document was indexed (above),
 +      // since it has a default value
 +      inPlaceUpdatedFields = callComputeInPlaceUpdatableFields(sdoc("id", "1", "_version_", 42L,
 +                                                                    altFieldWithDefault, map("set", 10)));
 +      assertTrue(field + " -> " + altFieldWithDefault, inPlaceUpdatedFields.contains(altFieldWithDefault));
 +      
 +      deleteAllAndCommit();
 +    }
 +  
 +    // Non in-place updates
 +    addAndGetVersion(sdoc("id", "1", "stored_i", "0"), params()); // setting up the dv
 +    assertTrue("stored field updated",
 +               callComputeInPlaceUpdatableFields(sdoc("id", "1", "_version_", 42L,
 +                                                      "stored_i", map("inc", 1))).isEmpty());
 +    
 +    assertTrue("full document update",
 +               callComputeInPlaceUpdatableFields(sdoc("id", "1", "_version_", 42L,
 +                                                      "inplace_updatable_int_with_default", "100")).isEmpty());
 +  
 +    assertTrue("non existent dynamic dv field updated first time",
 +               callComputeInPlaceUpdatableFields(sdoc("id", "1", "_version_", 42L,
 +                                                      "new_updatable_int_i_dvo", map("set", 10))).isEmpty());
 +    
 +    // After adding a full document with the dynamic dv field, in-place update should work
 +    addAndGetVersion(sdoc("id", "2", "new_updatable_int_i_dvo", "0"), params()); // setting up the dv
 +    if (random().nextBoolean()) {
 +      assertU(commit("softCommit", "false"));
 +    }
 +    inPlaceUpdatedFields = callComputeInPlaceUpdatableFields(sdoc("id", "2", "_version_", 42L,
 +                                                                  "new_updatable_int_i_dvo", map("set", 10)));
 +    assertTrue(inPlaceUpdatedFields.contains("new_updatable_int_i_dvo"));
 +
 +    // for copy fields, regardless of wether the source & target support inplace updates,
 +    // it won't be inplace if the DVs don't exist yet...
 +    assertTrue("inplace fields should be empty when doc has no copyfield src values yet",
 +               callComputeInPlaceUpdatableFields(sdoc("id", "1", "_version_", 42L,
 +                                                      "copyfield1_src__both_updatable", map("set", 1),
 +                                                      "copyfield2_src__only_src_updatable", map("set", 2))).isEmpty());
 +
 +    // now add a doc that *does* have the src field for each copyfield...
 +    addAndGetVersion(sdoc("id", "3",
 +                          "copyfield1_src__both_updatable", -13,
 +                          "copyfield2_src__only_src_updatable", -15), params()); 
 +    if (random().nextBoolean()) {
 +      assertU(commit("softCommit", "false"));
 +    }
 +    
 +    // If a supported dv field has a copyField target which is supported, it should be an in-place update
 +    inPlaceUpdatedFields = callComputeInPlaceUpdatableFields(sdoc("id", "3", "_version_", 42L,
 +                                                                  "copyfield1_src__both_updatable", map("set", 10)));
 +    assertTrue(inPlaceUpdatedFields.contains("copyfield1_src__both_updatable"));
 +
 +    // If a supported dv field has a copyField target which is not supported, it should not be an in-place update
 +    inPlaceUpdatedFields = callComputeInPlaceUpdatableFields(sdoc("id", "3", "_version_", 42L,
 +                                                                  "copyfield2_src__only_src_updatable", map("set", 10)));
 +    assertTrue(inPlaceUpdatedFields.isEmpty());
 +  }
 +
 +  @Test
 +  /**
 +   *  Test the @see {@link AtomicUpdateDocumentMerger#doInPlaceUpdateMerge(AddUpdateCommand,Set<String>)} 
 +   *  method is working fine
 +   */
 +  public void testDoInPlaceUpdateMerge() throws Exception {
 +    long version1 = addAndGetVersion(sdoc("id", "1", "title_s", "first"), null);
 +    long version2 = addAndGetVersion(sdoc("id", "2", "title_s", "second"), null);
 +    long version3 = addAndGetVersion(sdoc("id", "3", "title_s", "third"), null);
 +    assertU(commit("softCommit", "false"));
 +    assertQ(req("q", "*:*"), "//*[@numFound='3']");
 +
 +    // Adding a few in-place updates
 +    version1 = addAndAssertVersion(version1, "id", "1", "inplace_updatable_float", map("set", 200));
 +
 +    // Test the AUDM.doInPlaceUpdateMerge() method is working fine
 +    try (SolrQueryRequest req = req()) {
 +      AddUpdateCommand cmd = buildAddUpdateCommand(req, sdoc("id", "1", "_version_", 42L,
 +                                                             "inplace_updatable_float", map("inc", 10)));
 +      AtomicUpdateDocumentMerger docMerger = new AtomicUpdateDocumentMerger(req);
 +      assertTrue(docMerger.doInPlaceUpdateMerge(cmd, AtomicUpdateDocumentMerger.computeInPlaceUpdatableFields(cmd)));
 +      assertEquals(42L, cmd.getSolrInputDocument().getFieldValue("_version_"));
 +      assertEquals(42L, cmd.getSolrInputDocument().getFieldValue("_version_"));
 +      assertEquals(210f, cmd.getSolrInputDocument().getFieldValue("inplace_updatable_float"));
 +      // in-place merged doc shouldn't have non-inplace fields from the index/tlog
 +      assertFalse(cmd.getSolrInputDocument().containsKey("title_s"));
 +      assertEquals(version1, cmd.prevVersion);
 +    }
 +    
 +    // do a commit, and the same results should be repeated
 +    assertU(commit("softCommit", "false"));
 +
 +    // Test the AUDM.doInPlaceUpdateMerge() method is working fine
 +    try (SolrQueryRequest req = req()) {
 +      AddUpdateCommand cmd = buildAddUpdateCommand(req, sdoc("id", "1", "_version_", 42L,
 +                                                             "inplace_updatable_float", map("inc", 10)));
 +      AtomicUpdateDocumentMerger docMerger = new AtomicUpdateDocumentMerger(req);
 +      assertTrue(docMerger.doInPlaceUpdateMerge(cmd, AtomicUpdateDocumentMerger.computeInPlaceUpdatableFields(cmd)));
 +      assertEquals(42L, cmd.getSolrInputDocument().getFieldValue("_version_"));
 +      assertEquals(42L, cmd.getSolrInputDocument().getFieldValue("_version_"));
 +      assertEquals(210f, cmd.getSolrInputDocument().getFieldValue("inplace_updatable_float"));
 +      // in-place merged doc shouldn't have non-inplace fields from the index/tlog
 +      assertFalse(cmd.getSolrInputDocument().containsKey("title_s")); 
 +      assertEquals(version1, cmd.prevVersion);
 +    }
 +  }
 +  
 +  /** 
 +   * Helper method that sets up a req/cmd to run {@link AtomicUpdateDocumentMerger#computeInPlaceUpdatableFields} 
 +   * on the specified solr input document.
 +   */
 +  private static Set<String> callComputeInPlaceUpdatableFields(final SolrInputDocument sdoc) throws Exception {
 +    try (SolrQueryRequest req = req()) {
 +      AddUpdateCommand cmd = new AddUpdateCommand(req);
 +      cmd.solrDoc = sdoc;
 +      assertTrue(cmd.solrDoc.containsKey(DistributedUpdateProcessor.VERSION_FIELD));
 +      cmd.setVersion(Long.parseLong(cmd.solrDoc.getFieldValue(DistributedUpdateProcessor.VERSION_FIELD).toString()));
 +      return AtomicUpdateDocumentMerger.computeInPlaceUpdatableFields(cmd);
 +    }
 +  }
 +}