@@ -335,11 +335,11 @@ private static class SegmentLayout {
335335 }
336336
337337 @ Test
338- public void testUpgradeCoreIndexFailsWithNestedDocuments () throws Exception {
338+ public void testUpgradeCoreIndexFailsWithChildDocuments () throws Exception {
339339 final SolrCore core = h .getCore ();
340340 final String coreName = core .getName ();
341341
342- // Create a parent document with a child document (nested doc)
342+ // Create a parent document with a child document
343343 SolrInputDocument parentDoc = new SolrInputDocument ();
344344 parentDoc .addField ("id" , "100" );
345345 parentDoc .addField ("title" , "Parent Document" );
@@ -350,7 +350,7 @@ public void testUpgradeCoreIndexFailsWithNestedDocuments() throws Exception {
350350
351351 parentDoc .addChildDocument (childDoc );
352352
353- // Index the nested document
353+ // Index the parent+child document
354354 LocalSolrQueryRequest req = new LocalSolrQueryRequest (core , new ModifiableSolrParams ());
355355 try {
356356 AddUpdateCommand cmd = new AddUpdateCommand (req );
@@ -364,7 +364,7 @@ public void testUpgradeCoreIndexFailsWithNestedDocuments() throws Exception {
364364 // Verify documents were indexed (parent + child = 2 docs)
365365 assertQ (req ("q" , "*:*" ), "//result[@numFound='2']" );
366366
367- // Attempt to upgrade the index - should fail because of nested documents
367+ // Attempt to upgrade the index - should fail because of child documents
368368 CoreAdminHandler admin = new CoreAdminHandler (h .getCoreContainer ());
369369 try {
370370 final SolrQueryResponse resp = new SolrQueryResponse ();
@@ -380,13 +380,110 @@ public void testUpgradeCoreIndexFailsWithNestedDocuments() throws Exception {
380380 coreName ),
381381 resp ));
382382
383- // Verify the exception message indicates nested documents are not supported
383+ // Verify the exception message indicates child documents are not supported
384384 assertThat (
385385 thrown .getMessage (),
386- containsString ("does not support indexes containing nested documents" ));
386+ containsString ("does not support indexes containing child/ nested documents" ));
387387 } finally {
388388 admin .shutdown ();
389389 admin .close ();
390390 }
391391 }
392+
393+ @ Test
394+ public void testChildDocsDetection_noChildDocs () throws Exception {
395+ addDocsWithRandomUpdatesAndDeletes ();
396+
397+ final String coreName = h .getCore ().getName ();
398+ CoreAdminHandler admin = new CoreAdminHandler (h .getCoreContainer ());
399+ try {
400+ final SolrQueryResponse resp = new SolrQueryResponse ();
401+ admin .handleRequestBody (
402+ req (
403+ CoreAdminParams .ACTION ,
404+ CoreAdminParams .CoreAdminAction .UPGRADECOREINDEX .toString (),
405+ CoreAdminParams .CORE ,
406+ coreName ),
407+ resp );
408+ assertNull ("Unexpected exception: " + resp .getException (), resp .getException ());
409+ } finally {
410+ admin .shutdown ();
411+ admin .close ();
412+ }
413+ }
414+
415+ @ Test
416+ public void testChildDocsDetection_withChildDocs () throws Exception {
417+ addChildDoc ("100" , "101" );
418+ addDocsWithRandomUpdatesAndDeletes ();
419+
420+ final String coreName = h .getCore ().getName ();
421+ CoreAdminHandler admin = new CoreAdminHandler (h .getCoreContainer ());
422+ try {
423+ final SolrQueryResponse resp = new SolrQueryResponse ();
424+ SolrException thrown =
425+ assertThrows (
426+ SolrException .class ,
427+ () ->
428+ admin .handleRequestBody (
429+ req (
430+ CoreAdminParams .ACTION ,
431+ CoreAdminParams .CoreAdminAction .UPGRADECOREINDEX .toString (),
432+ CoreAdminParams .CORE ,
433+ coreName ),
434+ resp ));
435+ assertThat (
436+ thrown .getMessage (),
437+ containsString ("does not support indexes containing child/nested documents" ));
438+ } finally {
439+ admin .shutdown ();
440+ admin .close ();
441+ }
442+ }
443+
444+ /**
445+ * Add non-child docs with a random number of within-commit updates and deletes. This exercises
446+ * the false-positive scenario for child doc detection: updates and deletes leave behind deleted
447+ * entries in the same segment, causing multiple docs to share the same {@code _root_} value.
448+ *
449+ * <p>With NoMergePolicy and a 100MB RAM buffer (from SolrIndexConfig defaults), no flush or merge
450+ * occurs mid-batch, guaranteeing co-location in a single segment.
451+ */
452+ private void addDocsWithRandomUpdatesAndDeletes () {
453+ int numDocs = 10 ;
454+ for (int i = 0 ; i < numDocs ; i ++) {
455+ assertU (adoc ("id" , String .valueOf (i ), "title" , "doc" + i ));
456+ }
457+ int numUpdates = random ().nextInt (4 );
458+ for (int i = 0 ; i < numUpdates ; i ++) {
459+ assertU (adoc ("id" , String .valueOf (i ), "title" , "updated_doc" + i ));
460+ }
461+ int numDeletes = random ().nextInt (4 );
462+ for (int i = 0 ; i < numDeletes ; i ++) {
463+ assertU (delI (String .valueOf (numDocs - 1 - i )));
464+ }
465+ assertU (commit ("openSearcher" , "true" ));
466+ }
467+
468+ /** Index a parent document with a single child via the update handler. */
469+ private void addChildDoc (String parentId , String childId ) throws Exception {
470+ SolrCore core = h .getCore ();
471+ SolrInputDocument parentDoc = new SolrInputDocument ();
472+ parentDoc .addField ("id" , parentId );
473+ parentDoc .addField ("title" , "Parent " + parentId );
474+
475+ SolrInputDocument childDoc = new SolrInputDocument ();
476+ childDoc .addField ("id" , childId );
477+ childDoc .addField ("title" , "Child " + childId );
478+ parentDoc .addChildDocument (childDoc );
479+
480+ LocalSolrQueryRequest solrReq = new LocalSolrQueryRequest (core , new ModifiableSolrParams ());
481+ try {
482+ AddUpdateCommand cmd = new AddUpdateCommand (solrReq );
483+ cmd .solrDoc = parentDoc ;
484+ core .getUpdateHandler ().addDoc (cmd );
485+ } finally {
486+ solrReq .close ();
487+ }
488+ }
392489}
0 commit comments