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();
+ }
}