You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ch...@apache.org on 2026/05/25 10:20:58 UTC

(lucene) branch main updated: Reject duplicate column names in ColumnBatch (#16115)

This is an automated email from the ASF dual-hosted git repository.

ChrisHegarty pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/lucene.git


The following commit(s) were added to refs/heads/main by this push:
     new 0bd948017b8 Reject duplicate column names in ColumnBatch (#16115)
0bd948017b8 is described below

commit 0bd948017b8377b3f9a511a115e1ad6818aeb82e
Author: Tim Brooks <ti...@timbrooks.org>
AuthorDate: Mon May 25 04:20:52 2026 -0600

    Reject duplicate column names in ColumnBatch (#16115)
    
    A ColumnBatch where two columns share the same field name has no
    coherent semantics: each Column carries one IndexableFieldType, so two
    same-named columns would race to feed the same writers under one
    accumulated schema. The intended way to express multi-value is a single
    column whose tuple cursor emits multiple values per batch doc-id.
    
    IndexingChain.processBatch now reserves a fresh batchGen from
    nextFieldGen and rejects any column whose PerField was already tagged
    with that gen in the same batch's first pass. The check sits after the
    existing parentPf reserved-name check so reserved-name errors keep
    their dedicated message. Cross-batch reuse of a field name is
    unaffected since each batch consumes a strictly larger gen.
---
 lucene/CHANGES.txt                                 |  2 +
 .../apache/lucene/document/column/ColumnBatch.java |  3 +-
 .../org/apache/lucene/index/IndexingChain.java     | 10 +++-
 .../lucene/index/TestColumnBatchIndexing.java      | 63 ++++++++++++++++++++++
 4 files changed, 76 insertions(+), 2 deletions(-)

diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 8f89b5ab653..26fe0e8ea1c 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -507,6 +507,8 @@ Other
 
 * GITHUB#16073: Simplify SortedDocIDMerger.next(). (Tim Brooks)
 
+* GITHUB#16115: Reject duplicate column names in ColumnBatch. (Tim Brooks)
+
 ======================= Lucene 10.4.0 =======================
 
 API Changes
diff --git a/lucene/core/src/java/org/apache/lucene/document/column/ColumnBatch.java b/lucene/core/src/java/org/apache/lucene/document/column/ColumnBatch.java
index 9fb32741379..de39be245d7 100644
--- a/lucene/core/src/java/org/apache/lucene/document/column/ColumnBatch.java
+++ b/lucene/core/src/java/org/apache/lucene/document/column/ColumnBatch.java
@@ -33,7 +33,8 @@ public abstract class ColumnBatch {
 
   /**
    * Returns the columns in this batch. Each column represents a single field across the documents
-   * in the batch.
+   * in the batch. Within a batch, column names must be unique: multi-valued fields should be
+   * expressed as a single column whose tuple cursor emits multiple values per batch doc-id.
    */
   public abstract Iterable<Column> columns();
 }
diff --git a/lucene/core/src/java/org/apache/lucene/index/IndexingChain.java b/lucene/core/src/java/org/apache/lucene/index/IndexingChain.java
index c6997f0d0e7..2bafe6203d2 100644
--- a/lucene/core/src/java/org/apache/lucene/index/IndexingChain.java
+++ b/lucene/core/src/java/org/apache/lucene/index/IndexingChain.java
@@ -703,6 +703,7 @@ final class IndexingChain implements Accountable {
   void processBatch(int baseDocID, ColumnBatch columnBatch) throws IOException {
     final int numDocs = columnBatch.numDocs();
     boolean hasRowColumns = false;
+    long batchGen = nextFieldGen++;
 
     // First pass: validate all column schemas and initialize field infos
     for (Column column : columnBatch.columns()) {
@@ -728,6 +729,13 @@ final class IndexingChain implements Accountable {
         throw new IllegalArgumentException(
             "\"" + fieldName + "\" is a reserved field and should not be added to any document");
       }
+      if (pf.fieldGen == batchGen) {
+        throw new IllegalArgumentException(
+            "ColumnBatch contains more than one column for field \""
+                + fieldName
+                + "\"; multi-valued fields must use a single column with a sparse cursor");
+      }
+      pf.fieldGen = batchGen;
       validateColumnSchema(fieldName, pf, fieldType);
     }
 
@@ -839,7 +847,7 @@ final class IndexingChain implements Accountable {
       try {
         for (int i = 0; i < numRowCols; i++) {
           int head = heads[i];
-          if (head != DocIdSetIterator.NO_MORE_DOCS && head < batchDocID) {
+          if (head < batchDocID) {
             throw new IllegalArgumentException(
                 "Row column \""
                     + adapters[i].name()
diff --git a/lucene/core/src/test/org/apache/lucene/index/TestColumnBatchIndexing.java b/lucene/core/src/test/org/apache/lucene/index/TestColumnBatchIndexing.java
index dbf1ac7da38..d505b721495 100644
--- a/lucene/core/src/test/org/apache/lucene/index/TestColumnBatchIndexing.java
+++ b/lucene/core/src/test/org/apache/lucene/index/TestColumnBatchIndexing.java
@@ -1660,4 +1660,67 @@ public class TestColumnBatchIndexing extends LuceneTestCase {
     }
     assertEquals(DocIdSetIterator.NO_MORE_DOCS, bIt.nextDoc());
   }
+
+  public void testDuplicateColumnNameRejected() throws IOException {
+    Directory dir = newDirectory();
+    try (IndexWriter w = new IndexWriter(dir, newIndexWriterConfig())) {
+      int[] ids = {0, 1};
+      long[] vals = {1L, 2L};
+      IllegalArgumentException e =
+          expectThrows(
+              IllegalArgumentException.class,
+              () ->
+                  w.addBatch(
+                      simpleBatch(
+                          2,
+                          new ArrayLongColumn("field", NumericDocValuesField.TYPE, ids, vals),
+                          new ArrayLongColumn("field", NumericDocValuesField.TYPE, ids, vals))));
+      assertTrue(e.getMessage(), e.getMessage().contains("field"));
+
+      // Writer still usable after validation failure.
+      w.addBatch(
+          simpleBatch(
+              1,
+              new ArrayLongColumn("v", NumericDocValuesField.TYPE, new int[] {0}, new long[] {7})));
+      w.forceMerge(1);
+      DirectoryReader r = DirectoryReader.open(w);
+      // After forceMerge, fully-deleted segments are pruned; only the recovery doc remains.
+      assertEquals(1, r.numDocs());
+      LeafReader leaf = getOnlyLeafReader(r);
+      NumericDocValues dv = leaf.getNumericDocValues("v");
+      assertNotNull(dv);
+      assertTrue(dv.nextDoc() != DocIdSetIterator.NO_MORE_DOCS);
+      assertEquals(7, dv.longValue());
+      r.close();
+    }
+    dir.close();
+  }
+
+  public void testSameFieldNameAcrossBatchesAllowed() throws IOException {
+    Directory dir = newDirectory();
+    try (IndexWriter w = new IndexWriter(dir, newIndexWriterConfig())) {
+      w.addBatch(
+          simpleBatch(
+              1,
+              new ArrayLongColumn(
+                  "f", NumericDocValuesField.TYPE, new int[] {0}, new long[] {1L})));
+      w.addBatch(
+          simpleBatch(
+              1,
+              new ArrayLongColumn(
+                  "f", NumericDocValuesField.TYPE, new int[] {0}, new long[] {2L})));
+      w.forceMerge(1);
+      try (DirectoryReader r = DirectoryReader.open(w)) {
+        LeafReader leaf = getOnlyLeafReader(r);
+        NumericDocValues dv = leaf.getNumericDocValues("f");
+        assertNotNull(dv);
+        assertEquals(0, dv.nextDoc());
+        assertEquals(1L, dv.longValue());
+        assertEquals(1, dv.nextDoc());
+        assertEquals(2L, dv.longValue());
+        assertEquals(DocIdSetIterator.NO_MORE_DOCS, dv.nextDoc());
+      }
+    }
+    dir.close();
+  }
 }