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

xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 04a294f3e2e Add BSON (MongoDB) input format support (#18950)
04a294f3e2e is described below

commit 04a294f3e2e393609e256efa8e2b95e2e20f5bfc
Author: Xiang Fu <[email protected]>
AuthorDate: Fri Jul 10 07:50:58 2026 +0200

    Add BSON (MongoDB) input format support (#18950)
    
    * Add BSON (MongoDB) input format support
    
    Add a new `pinot-bson` input-format plugin for reading MongoDB BSON data,
    covering both batch and streaming ingestion:
    
    - BSONRecordReader reads mongodump-style files (concatenated length-prefixed
      BSON documents); gzip files are supported transparently.
    - BSONMessageDecoder decodes one BSON document per stream message.
    - BSONRecordExtractor maps BSON's types onto the RecordExtractor contract:
      ObjectId -> hex String, DateTime -> java.sql.Timestamp, Decimal128 ->
      BigDecimal (NaN/Infinity -> null), Binary -> byte[], embedded documents ->
      Map, arrays -> Object[], with a toString() fallback for rare/deprecated 
types.
    
    BSON is registered in the FileFormat enum and RecordReaderFactory so
    `dataFormat: bson` works in batch ingestion out of the box, and the module 
is
    wired into the BOM, distribution assembly, tools, and LICENSE-binary. Uses 
the
    standalone org.mongodb:bson library (no MongoDB driver / network 
dependency).
    
    * Address PR review comments
    
    - BSONRecordReader: on a truncated/corrupt tail, emit the already-read valid
      record and defer the fetch error to the following next() call (via a 
stashed
      IOException surfaced through hasNext()/next()), instead of discarding the 
last
      valid record. Matches the emit-then-error behavior of JSONRecordReader.
    - BSONMessageDecoderTest: use assertSame instead of assertTrue(result == 
row).
    - Add a reader regression test covering the truncated-tail case.
    
    * Address review: bound frame length, handle negative-zero Decimal128
    
    - BSONRecordReader: reject a frame length above the 16MB BSON maximum. A 
corrupt
      length prefix (e.g. 0x7FFFFFFF) previously reached `new byte[length]` and 
raised
      OutOfMemoryError, an Error that escapes the 
catch(RuntimeException)/catch(IOException)
      recovery paths and aborts the segment build. It now surfaces as a 
recoverable
      IOException before any allocation.
    - BSONRecordExtractor: negative-zero Decimal128 (a legal Mongo value, at 
any exponent)
      has isNaN()==false and isInfinite()==false, so it slipped the guard and 
threw
      ArithmeticException out of extract(). It now converts to BigDecimal.ZERO. 
There is no
      public negative-zero predicate and Decimal128.parse("-0.00") != 
NEGATIVE_ZERO, so the
      exception is caught rather than pre-checked.
    - BSONRecordReader.open(): close the input stream if the first read fails, 
matching
      JSONRecordReader.init().
    - BSONUtils: hoist the immutable DecoderContext to a static final field.
    - BSONRecordExtractor javadoc: correct the toString() fallback wording -- 
it also covers
      the internal Timestamp type and the 5.x binary vector types.
    - Add regression tests for both fixed edge cases.
    
    * Address review: map BsonTimestamp, pin fallback strings, cover bson 
registration
    
    - BsonTimestamp (the oplog `ts` / change-stream `clusterTime` field) 
previously
      fell through to value.toString() and ingested as the debug string
      "Timestamp{value=..., seconds=..., inc=...}". Convert it to 
java.sql.Timestamp
      at second granularity, matching MongoDB's $toDate. The intra-second 
ordinal
      counter is not representable and is dropped.
    
    - The remaining toString() fallbacks (MinKey, MaxKey, Symbol, Code,
      BsonRegularExpression) render via formats that org.mongodb:bson does not
      document, yet they are baked into segment data. 
testExoticTypeFallsBackToToString
      asserted extract(x).equals(x.toString()), which is self-referential and 
pins
      nothing. Pin the literal strings so a driver upgrade that reformats them 
fails
      the build instead of silently rewriting segments.
    
    - Add a round-trip test for BSON binary subtypes 0x03/0x04 (how MongoDB 
stores
      UUIDs). They currently decode to Binary -> byte[] because DocumentCodec 
defaults
      to UuidRepresentation.UNSPECIFIED; pin that so a default flip to 
java.util.UUID
      (which would change the column from BYTES to a 36-char STRING) is caught.
    
    - RecordReaderFactoryTest enumerates every format's default reader class but
      omitted bson, leaving `dataFormat: bson` resolution unverified and a typo 
in
      DEFAULT_BSON_RECORD_READER_CLASS a runtime-only failure.
    
    - Document BSONUtils' thread safety (the shared 
DocumentCodec/DecoderContext are
      immutable, so concurrent ingestion threads may decode). Note that 
Decimal128
      extends Number, so its branch must stay above the Number pass-through.
    
    - Use /// Javadoc consistently across the plugin.
    
    * Read the BsonTimestamp seconds field as unsigned
    
    BSON stores a Timestamp's seconds component as an unsigned 32-bit field, but
    BsonTimestamp#getTime() returns it as a signed int. From 
2038-01-19T03:14:08Z
    onward the high bit is set, so widening the raw value sign-extended it: a
    2039-01-01 oplog timestamp ingested as 1902-11-25.
    
    Mask before widening. This matches the MongoDB server, which reads those 
seconds
    as unsigned.
---
 AGENTS.md                                          |   1 +
 CLAUDE.md                                          |   1 +
 LICENSE-binary                                     |   1 +
 pinot-bom/pom.xml                                  |   5 +
 pinot-distribution/pinot-assembly.xml              |   6 +
 .../pinot-input-format/{ => pinot-bson}/pom.xml    |  42 ++-
 .../inputformat/bson/BSONMessageDecoder.java       |  67 +++++
 .../inputformat/bson/BSONRecordExtractor.java      | 147 +++++++++++
 .../plugin/inputformat/bson/BSONRecordReader.java  | 168 ++++++++++++
 .../pinot/plugin/inputformat/bson/BSONUtils.java   |  57 ++++
 .../inputformat/bson/BSONMessageDecoderTest.java   | 153 +++++++++++
 .../inputformat/bson/BSONRecordExtractorTest.java  | 290 +++++++++++++++++++++
 .../inputformat/bson/BSONRecordReaderTest.java     | 212 +++++++++++++++
 .../pinot-bson/src/test/resources/log4j2.xml       |  36 +++
 pinot-plugins/pinot-input-format/pom.xml           |   1 +
 .../apache/pinot/spi/data/readers/FileFormat.java  |   2 +-
 .../spi/data/readers/RecordReaderFactory.java      |   2 +
 .../spi/data/readers/RecordReaderFactoryTest.java  |   4 +
 pinot-tools/pom.xml                                |   4 +
 pom.xml                                            |   6 +
 20 files changed, 1178 insertions(+), 27 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index eec4f372c2d..cd62355a0d7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -48,6 +48,7 @@ repo. It is intentionally short and focused on day-to-day 
work.
   - pinot-arrow: Apache Arrow input format support.
   - pinot-avro: Avro input format support.
   - pinot-avro-base: shared Avro utilities and base classes.
+  - pinot-bson: MongoDB BSON input format support.
   - pinot-clp-log: CLP log input format support.
   - pinot-confluent-avro: Confluent Schema Registry Avro input support.
   - pinot-confluent-json: Confluent Schema Registry JSON input support.
diff --git a/CLAUDE.md b/CLAUDE.md
index 25cd4c2de28..d9abfc33e77 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -44,6 +44,7 @@ Apache Pinot is a real-time distributed OLAP datastore for 
low-latency analytics
   - `pinot-arrow`: Apache Arrow input format support.
   - `pinot-avro`: Avro input format support.
   - `pinot-avro-base`: shared Avro utilities and base classes.
+  - `pinot-bson`: MongoDB BSON input format support.
   - `pinot-clp-log`: CLP log input format support.
   - `pinot-confluent-avro`: Confluent Schema Registry Avro input support.
   - `pinot-confluent-json`: Confluent Schema Registry JSON input support.
diff --git a/LICENSE-binary b/LICENSE-binary
index b7c9486ba7a..8d688bb0936 100644
--- a/LICENSE-binary
+++ b/LICENSE-binary
@@ -531,6 +531,7 @@ org.jooq:joou-java-6:0.9.5
 org.jspecify:jspecify:1.0.0
 org.locationtech.proj4j:proj4j:1.2.2
 org.lz4:lz4-java:1.8.0
+org.mongodb:bson:5.6.1
 org.quartz-scheduler:quartz:2.5.2
 org.roaringbitmap:RoaringBitmap:1.6.12
 org.scala-lang.modules:scala-collection-compat_2.13:2.10.0
diff --git a/pinot-bom/pom.xml b/pinot-bom/pom.xml
index aa67b76074b..4d8566f29c0 100644
--- a/pinot-bom/pom.xml
+++ b/pinot-bom/pom.xml
@@ -324,6 +324,11 @@
         <artifactId>pinot-json</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.pinot</groupId>
+        <artifactId>pinot-bson</artifactId>
+        <version>${project.version}</version>
+      </dependency>
       <dependency>
         <groupId>org.apache.pinot</groupId>
         <artifactId>pinot-csv</artifactId>
diff --git a/pinot-distribution/pinot-assembly.xml 
b/pinot-distribution/pinot-assembly.xml
index 80747081d3e..13940e20ca3 100644
--- a/pinot-distribution/pinot-assembly.xml
+++ b/pinot-distribution/pinot-assembly.xml
@@ -152,6 +152,12 @@
       </source>
       
<destName>plugins/pinot-input-format/pinot-json/pinot-json-${project.version}-shaded.jar</destName>
     </file>
+    <file>
+      <source>
+        
${pinot.root}/pinot-plugins/pinot-input-format/pinot-bson/target/pinot-bson-${project.version}-shaded.jar
+      </source>
+      
<destName>plugins/pinot-input-format/pinot-bson/pinot-bson-${project.version}-shaded.jar</destName>
+    </file>
     <file>
       
<source>${pinot.root}/pinot-plugins/pinot-input-format/pinot-orc/target/pinot-orc-${project.version}-shaded.jar
       </source>
diff --git a/pinot-plugins/pinot-input-format/pom.xml 
b/pinot-plugins/pinot-input-format/pinot-bson/pom.xml
similarity index 60%
copy from pinot-plugins/pinot-input-format/pom.xml
copy to pinot-plugins/pinot-input-format/pinot-bson/pom.xml
index 43873ae79e8..b4dd0dd139d 100644
--- a/pinot-plugins/pinot-input-format/pom.xml
+++ b/pinot-plugins/pinot-input-format/pinot-bson/pom.xml
@@ -22,42 +22,32 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
   <modelVersion>4.0.0</modelVersion>
   <parent>
-    <artifactId>pinot-plugins</artifactId>
+    <artifactId>pinot-input-format</artifactId>
     <groupId>org.apache.pinot</groupId>
     <version>1.6.0-SNAPSHOT</version>
   </parent>
 
-  <artifactId>pinot-input-format</artifactId>
-  <packaging>pom</packaging>
-  <name>Pinot Input Format</name>
+  <artifactId>pinot-bson</artifactId>
+  <name>Pinot BSON</name>
   <url>https://pinot.apache.org/</url>
   <properties>
-    <pinot.root>${basedir}/../..</pinot.root>
-    <plugin.type>pinot-input-format</plugin.type>
+    <pinot.root>${basedir}/../../..</pinot.root>
+    <shade.phase.prop>package</shade.phase.prop>
   </properties>
 
-  <modules>
-    <module>pinot-arrow</module>
-    <module>pinot-avro</module>
-    <module>pinot-avro-base</module>
-    <module>pinot-clp-log</module>
-    <module>pinot-confluent-avro</module>
-    <module>pinot-confluent-json</module>
-    <module>pinot-confluent-protobuf</module>
-    <module>pinot-orc</module>
-    <module>pinot-json</module>
-    <module>pinot-parquet</module>
-    <module>pinot-csv</module>
-    <module>pinot-thrift</module>
-    <module>pinot-protobuf</module>
-  </modules>
-
   <dependencies>
     <dependency>
-      <groupId>org.apache.pinot</groupId>
-      <artifactId>pinot-spi</artifactId>
-      <type>test-jar</type>
-      <scope>test</scope>
+      <groupId>org.mongodb</groupId>
+      <artifactId>bson</artifactId>
     </dependency>
   </dependencies>
+
+  <profiles>
+    <profile>
+      <id>pinot-fastdev</id>
+      <properties>
+        <shade.phase.prop>none</shade.phase.prop>
+      </properties>
+    </profile>
+  </profiles>
 </project>
diff --git 
a/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONMessageDecoder.java
 
b/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONMessageDecoder.java
new file mode 100644
index 00000000000..4a7df0b7c86
--- /dev/null
+++ 
b/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONMessageDecoder.java
@@ -0,0 +1,67 @@
+/**
+ * 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.pinot.plugin.inputformat.bson;
+
+import java.util.Map;
+import java.util.Set;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.data.readers.RecordExtractor;
+import org.apache.pinot.spi.plugin.PluginManager;
+import org.apache.pinot.spi.stream.StreamMessageDecoder;
+import org.bson.Document;
+
+
+/// An implementation of [StreamMessageDecoder] to read BSON records from a 
stream. Each message payload is
+/// expected to be a single binary-encoded BSON document. The document is 
decoded into a [Document] (a
+/// `Map<String, Object>`) and handed to a [BSONRecordExtractor].
+public class BSONMessageDecoder implements StreamMessageDecoder<byte[]> {
+  private static final String BSON_RECORD_EXTRACTOR_CLASS =
+      "org.apache.pinot.plugin.inputformat.bson.BSONRecordExtractor";
+
+  private RecordExtractor<Map<String, Object>> _bsonRecordExtractor;
+
+  @Override
+  public void init(Map<String, String> props, Set<String> fieldsToRead, String 
topicName)
+      throws Exception {
+    String recordExtractorClass = null;
+    if (props != null) {
+      recordExtractorClass = props.get(RECORD_EXTRACTOR_CONFIG_KEY);
+    }
+    if (recordExtractorClass == null) {
+      recordExtractorClass = BSON_RECORD_EXTRACTOR_CLASS;
+    }
+    _bsonRecordExtractor = 
PluginManager.get().createInstance(recordExtractorClass);
+    _bsonRecordExtractor.init(fieldsToRead, null);
+  }
+
+  @Override
+  public GenericRow decode(byte[] payload, GenericRow destination) {
+    return decode(payload, 0, payload.length, destination);
+  }
+
+  @Override
+  public GenericRow decode(byte[] payload, int offset, int length, GenericRow 
destination) {
+    try {
+      Document document = BSONUtils.decodeDocument(payload, offset, length);
+      return _bsonRecordExtractor.extract(document, destination);
+    } catch (Exception e) {
+      throw new RuntimeException("Caught exception while decoding BSON 
record", e);
+    }
+  }
+}
diff --git 
a/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordExtractor.java
 
b/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordExtractor.java
new file mode 100644
index 00000000000..050e215520d
--- /dev/null
+++ 
b/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordExtractor.java
@@ -0,0 +1,147 @@
+/**
+ * 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.pinot.plugin.inputformat.bson;
+
+import com.google.common.collect.Maps;
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import org.apache.pinot.spi.data.readers.BaseRecordExtractor;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.bson.BsonTimestamp;
+import org.bson.types.Binary;
+import org.bson.types.Decimal128;
+import org.bson.types.ObjectId;
+
+
+/// Extracts a Pinot [GenericRow] from a decoded BSON document 
(`org.bson.Document`, which is a
+/// `Map<String, Object>`). Values are the Java objects produced by the 
standard MongoDB
+/// [org.bson.codecs.DocumentCodec].
+///
+/// **BSON type → Java output type:**
+/// - `Double` / `Int32` / `Int64` / `Boolean` / `String` → same boxed type 
(pass-through)
+/// - `Document` (embedded) → `Map<String, Object>` (values recursively 
converted)
+/// - `Array` → `Object[]` (elements recursively converted)
+/// - `ObjectId` → `String` (24-char hex)
+/// - `DateTime` → `java.sql.Timestamp`
+/// - `Decimal128` → `BigDecimal` (`NaN` / `Infinity` → `null`, as 
`BigDecimal` cannot represent them; negative
+///   zero → `BigDecimal.ZERO`)
+/// - `Timestamp` (the internal replication type, e.g. the oplog / 
change-stream `ts` and `clusterTime` fields)
+///   → `java.sql.Timestamp` at second granularity, matching MongoDB's own 
`$toDate`. The seconds field is
+///   unsigned, so it is read as such and remains correct past 2038. The 
ordinal counter that disambiguates
+///   operations within the same second is not representable in a `Timestamp` 
and is dropped, so this value
+///   orders operations only to the second.
+/// - `Binary` → `byte[]`. This includes the UUID subtypes (`0x03` legacy, 
`0x04` standard): the standard codec
+///   is constructed with `UuidRepresentation.UNSPECIFIED`, so it hands back 
the raw 16 bytes rather than a
+///   `java.util.UUID`.
+/// - `null` → `null`
+///
+/// Every other BSON type falls back to `value.toString()` — the deprecated 
`Symbol` / `Undefined` / `DBPointer`
+/// types, JavaScript `Code`, regular expressions, `MinKey` / `MaxKey`, and 
the binary vector types
+/// (`Float32BinaryVector` and friends), which the standard codec decodes to 
their own classes rather than to
+/// `Binary`. These renderings are not a documented contract of 
`org.mongodb:bson`, so `BSONRecordExtractorTest`
+/// pins the exact strings: a driver upgrade that changes them must fail the 
build rather than silently change
+/// the contents of every segment built from such a document.
+///
+/// The converted values follow the shared `RecordExtractor` contract, so the 
downstream data-type transformer
+/// coerces them to the declared column type.
+public class BSONRecordExtractor extends BaseRecordExtractor<Map<String, 
Object>> {
+
+  @Override
+  public GenericRow extract(Map<String, Object> from, GenericRow to) {
+    if (_extractAll) {
+      for (Map.Entry<String, Object> entry : from.entrySet()) {
+        Object value = entry.getValue();
+        to.putValue(entry.getKey(), value != null ? convert(value) : null);
+      }
+    } else {
+      for (String fieldName : _fields) {
+        Object value = from.get(fieldName);
+        to.putValue(fieldName, value != null ? convert(value) : null);
+      }
+    }
+    return to;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Object convert(Object value) {
+    if (value instanceof Map) {
+      return convertMap((Map<String, Object>) value);
+    }
+    if (value instanceof List) {
+      return convertList((List<Object>) value);
+    }
+    if (value instanceof ObjectId) {
+      return ((ObjectId) value).toHexString();
+    }
+    if (value instanceof Date) {
+      return new Timestamp(((Date) value).getTime());
+    }
+    if (value instanceof BsonTimestamp) {
+      // getTime() is the seconds component; the ordinal counter in the low 32 
bits is dropped. BSON stores
+      // those seconds unsigned but getTime() returns a signed int, so mask 
before widening -- otherwise every
+      // timestamp from 2038-01-19 onward wraps to a pre-1970 date.
+      return new Timestamp((((BsonTimestamp) value).getTime() & 0xFFFFFFFFL) * 
1000L);
+    }
+    // NOTE: Decimal128 extends Number, so this must stay above the Number 
pass-through below.
+    if (value instanceof Decimal128) {
+      Decimal128 decimal128 = (Decimal128) value;
+      // NaN / Infinity are legal Decimal128 values with no BigDecimal 
representation; surface them as null.
+      if (decimal128.isNaN() || decimal128.isInfinite()) {
+        return null;
+      }
+      try {
+        return decimal128.bigDecimalValue();
+      } catch (ArithmeticException e) {
+        // The only remaining value bigDecimalValue() rejects is negative zero 
(legal in BSON, at any exponent,
+        // and not detectable via a public predicate). It is numerically zero.
+        return BigDecimal.ZERO;
+      }
+    }
+    if (value instanceof Binary) {
+      return ((Binary) value).getData();
+    }
+    // Double / Integer / Long / String / Boolean pass through; any other BSON 
type uses its string form.
+    if (value instanceof Number || value instanceof String || value instanceof 
Boolean) {
+      return value;
+    }
+    return value.toString();
+  }
+
+  private static Object[] convertList(List<Object> list) {
+    int numValues = list.size();
+    Object[] result = new Object[numValues];
+    for (int i = 0; i < numValues; i++) {
+      Object value = list.get(i);
+      result[i] = value != null ? convert(value) : null;
+    }
+    return result;
+  }
+
+  private static Map<String, Object> convertMap(Map<String, Object> map) {
+    Map<String, Object> result = Maps.newHashMapWithExpectedSize(map.size());
+    for (Map.Entry<String, Object> entry : map.entrySet()) {
+      Object value = entry.getValue();
+      result.put(entry.getKey(), value != null ? convert(value) : null);
+    }
+    return result;
+  }
+}
diff --git 
a/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordReader.java
 
b/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordReader.java
new file mode 100644
index 00000000000..d30854df337
--- /dev/null
+++ 
b/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordReader.java
@@ -0,0 +1,168 @@
+/**
+ * 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.pinot.plugin.inputformat.bson;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.data.readers.RecordFetchException;
+import org.apache.pinot.spi.data.readers.RecordReader;
+import org.apache.pinot.spi.data.readers.RecordReaderConfig;
+import org.apache.pinot.spi.data.readers.RecordReaderUtils;
+import org.bson.Document;
+
+
+/// Record reader for a BSON file: a concatenation of framed BSON documents 
(the `mongodump` layout), each
+/// self-delimited by a leading little-endian int32 byte length. Documents are 
read sequentially, with the next
+/// document's bytes fetched ahead so [#hasNext] does not perform I/O. 
GZIP-compressed files are supported.
+///
+/// Not thread-safe: a reader instance is single-threaded, like every other 
[RecordReader].
+public class BSONRecordReader implements RecordReader {
+  // Minimum size of a BSON document: 4-byte length prefix + 1-byte 
terminating NUL of an empty document.
+  private static final int MIN_DOCUMENT_LENGTH = 5;
+  // MongoDB caps BSON documents at 16MB. Without an upper bound, a corrupt 
length prefix (up to ~2GB) would
+  // reach `new byte[length]` and raise OutOfMemoryError -- an Error, so it 
would escape the recoverable
+  // exception handling in next()/advance() and abort the whole segment build.
+  private static final int MAX_DOCUMENT_LENGTH = 16 * 1024 * 1024;
+
+  private File _dataFile;
+  private BSONRecordExtractor _recordExtractor;
+  private InputStream _inputStream;
+  // Bytes of the next framed document, or null once the stream is exhausted.
+  private byte[] _nextDocument;
+  // A read error hit while fetching the next document. Deferred so the 
current record is still emitted; it
+  // surfaces on the following next() call rather than discarding an 
already-read valid record.
+  private IOException _fetchError;
+
+  public BSONRecordReader() {
+  }
+
+  @Override
+  public void init(File dataFile, @Nullable Set<String> fieldsToRead, 
@Nullable RecordReaderConfig recordReaderConfig)
+      throws IOException {
+    _dataFile = dataFile;
+    _recordExtractor = new BSONRecordExtractor();
+    _recordExtractor.init(fieldsToRead, null);
+    open();
+  }
+
+  private void open()
+      throws IOException {
+    _inputStream = RecordReaderUtils.getBufferedInputStream(_dataFile);
+    _fetchError = null;
+    try {
+      _nextDocument = readNextDocument();
+    } catch (IOException e) {
+      _inputStream.close();
+      throw e;
+    }
+  }
+
+  @Override
+  public boolean hasNext() {
+    return _nextDocument != null || _fetchError != null;
+  }
+
+  @Override
+  public GenericRow next(GenericRow reuse)
+      throws IOException {
+    if (_fetchError != null) {
+      IOException error = _fetchError;
+      _fetchError = null;
+      throw new RecordFetchException("Failed to read next BSON record", error);
+    }
+    byte[] documentBytes = _nextDocument;
+    Document document;
+    try {
+      document = BSONUtils.decodeDocument(documentBytes);
+    } catch (RuntimeException e) {
+      // Corrupt frame: advance past it (bytes already consumed) so we don't 
retry, then report a parse error.
+      advance();
+      throw new IOException("Failed to decode BSON record", e);
+    }
+    _recordExtractor.extract(document, reuse);
+    advance();
+    return reuse;
+  }
+
+  /// Advances the look-ahead to the next framed document. A read error is 
stashed rather than thrown so the
+  /// record just returned by next() is still emitted; the error surfaces on 
the following next() call.
+  private void advance() {
+    try {
+      _nextDocument = readNextDocument();
+    } catch (IOException e) {
+      _nextDocument = null;
+      _fetchError = e;
+    }
+  }
+
+  /// Reads the next framed BSON document in full, or returns `null` at a 
clean end-of-stream. Throws when the
+  /// stream ends partway through a document, or the length prefix is invalid.
+  @Nullable
+  private byte[] readNextDocument()
+      throws IOException {
+    int b0 = _inputStream.read();
+    if (b0 == -1) {
+      return null;
+    }
+    int b1 = _inputStream.read();
+    int b2 = _inputStream.read();
+    int b3 = _inputStream.read();
+    if ((b1 | b2 | b3) < 0) {
+      throw new IOException("Truncated BSON document: incomplete length 
prefix");
+    }
+    // BSON length prefix is a little-endian int32 inclusive of these 4 bytes.
+    int length = (b0 & 0xFF) | ((b1 & 0xFF) << 8) | ((b2 & 0xFF) << 16) | ((b3 
& 0xFF) << 24);
+    if (length < MIN_DOCUMENT_LENGTH || length > MAX_DOCUMENT_LENGTH) {
+      throw new IOException(
+          "Invalid BSON document length: " + length + " (expected " + 
MIN_DOCUMENT_LENGTH + ".."
+              + MAX_DOCUMENT_LENGTH + " bytes)");
+    }
+    byte[] document = new byte[length];
+    document[0] = (byte) b0;
+    document[1] = (byte) b1;
+    document[2] = (byte) b2;
+    document[3] = (byte) b3;
+    int bytesRead = 4;
+    while (bytesRead < length) {
+      int count = _inputStream.read(document, bytesRead, length - bytesRead);
+      if (count == -1) {
+        throw new IOException("Truncated BSON document: expected " + length + 
" bytes");
+      }
+      bytesRead += count;
+    }
+    return document;
+  }
+
+  @Override
+  public void rewind()
+      throws IOException {
+    _inputStream.close();
+    open();
+  }
+
+  @Override
+  public void close()
+      throws IOException {
+    _inputStream.close();
+  }
+}
diff --git 
a/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONUtils.java
 
b/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONUtils.java
new file mode 100644
index 00000000000..3703ef2ca94
--- /dev/null
+++ 
b/pinot-plugins/pinot-input-format/pinot-bson/src/main/java/org/apache/pinot/plugin/inputformat/bson/BSONUtils.java
@@ -0,0 +1,57 @@
+/**
+ * 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.pinot.plugin.inputformat.bson;
+
+import java.nio.ByteBuffer;
+import org.bson.BsonBinaryReader;
+import org.bson.Document;
+import org.bson.codecs.DecoderContext;
+import org.bson.codecs.DocumentCodec;
+
+
+/// Decodes a single BSON binary document into an `org.bson.Document` (a 
`Map<String, Object>`). Shared by
+/// [BSONRecordReader] (batch) and [BSONMessageDecoder] (streaming).
+///
+/// Thread-safe: the shared [DocumentCodec] and [DecoderContext] are immutable 
after construction, and each
+/// [#decodeDocument] call reads through a reader over its own buffer. Callers 
on different ingestion threads
+/// may decode concurrently.
+///
+/// A malformed document raises `org.bson.BsonSerializationException` (a 
`RuntimeException`). Bogus internal
+/// length prefixes cannot over-allocate: the reader is bounded by the 
supplied `[offset, offset + length)`
+/// slice.
+public final class BSONUtils {
+  private static final DocumentCodec DOCUMENT_CODEC = new DocumentCodec();
+  private static final DecoderContext DECODER_CONTEXT = 
DecoderContext.builder().build();
+
+  private BSONUtils() {
+  }
+
+  /// Decodes a whole BSON document from the given byte array.
+  public static Document decodeDocument(byte[] bytes) {
+    return decodeDocument(bytes, 0, bytes.length);
+  }
+
+  /// Decodes a single BSON document from the `[offset, offset + length)` 
sub-range of the given byte array.
+  public static Document decodeDocument(byte[] bytes, int offset, int length) {
+    ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, length).slice();
+    try (BsonBinaryReader reader = new BsonBinaryReader(buffer)) {
+      return DOCUMENT_CODEC.decode(reader, DECODER_CONTEXT);
+    }
+  }
+}
diff --git 
a/pinot-plugins/pinot-input-format/pinot-bson/src/test/java/org/apache/pinot/plugin/inputformat/bson/BSONMessageDecoderTest.java
 
b/pinot-plugins/pinot-input-format/pinot-bson/src/test/java/org/apache/pinot/plugin/inputformat/bson/BSONMessageDecoderTest.java
new file mode 100644
index 00000000000..77a8ac2b031
--- /dev/null
+++ 
b/pinot-plugins/pinot-input-format/pinot-bson/src/test/java/org/apache/pinot/plugin/inputformat/bson/BSONMessageDecoderTest.java
@@ -0,0 +1,153 @@
+/**
+ * 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.pinot.plugin.inputformat.bson;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.bson.BsonBinaryWriter;
+import org.bson.BsonTimestamp;
+import org.bson.Document;
+import org.bson.codecs.DocumentCodec;
+import org.bson.codecs.EncoderContext;
+import org.bson.io.BasicOutputBuffer;
+import org.bson.types.Decimal128;
+import org.bson.types.ObjectId;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+
+
+public class BSONMessageDecoderTest {
+
+  @Test
+  public void testDecodeScalarsAndLogicalTypes()
+      throws Exception {
+    ObjectId objectId = new ObjectId("5f2b1c0e1c9d440000a1b2c3");
+    long epochMillis = 1_588_469_340_000L;
+    Document document = new Document()
+        .append("id", objectId)
+        .append("name", "widget")
+        .append("count", 7)
+        .append("views", 1_588_469_340_000L)
+        .append("price", 9.5d)
+        .append("active", true)
+        .append("amount", Decimal128.parse("123.456"))
+        .append("createdAt", new Date(epochMillis))
+        // The oplog / change-stream replication timestamp, the field a 
Kafka-forwarded change stream is most
+        // likely to use as the Pinot time column.
+        .append("ts", new BsonTimestamp((int) (epochMillis / 1000), 3));
+
+    Set<String> fields = document.keySet();
+    BSONMessageDecoder decoder = new BSONMessageDecoder();
+    decoder.init(Map.of(), fields, "testTopic");
+
+    GenericRow row = new GenericRow();
+    decoder.decode(encode(document), row);
+
+    assertEquals(row.getValue("id"), objectId.toHexString());
+    assertEquals(row.getValue("name"), "widget");
+    assertEquals(row.getValue("count"), 7);
+    assertEquals(row.getValue("views"), 1_588_469_340_000L);
+    assertEquals(row.getValue("price"), 9.5d);
+    assertEquals(row.getValue("active"), true);
+    assertEquals(row.getValue("amount"), new BigDecimal("123.456"));
+    assertEquals(row.getValue("createdAt"), new Timestamp(epochMillis));
+    assertEquals(row.getValue("ts"), new Timestamp(epochMillis));
+  }
+
+  @Test
+  public void testDecodeNestedDocumentAndArray()
+      throws Exception {
+    Document document = new Document()
+        .append("tags", List.of("a", "b", "c"))
+        .append("meta", new Document("region", "us-east").append("shard", 3));
+
+    Set<String> fields = document.keySet();
+    BSONMessageDecoder decoder = new BSONMessageDecoder();
+    decoder.init(Map.of(), fields, "testTopic");
+
+    GenericRow row = new GenericRow();
+    decoder.decode(encode(document), row);
+
+    assertEquals((Object[]) row.getValue("tags"), new Object[]{"a", "b", "c"});
+    Map<?, ?> meta = (Map<?, ?>) row.getValue("meta");
+    assertEquals(meta.get("region"), "us-east");
+    assertEquals(meta.get("shard"), 3);
+  }
+
+  @Test
+  public void testDecodeWithFieldProjection()
+      throws Exception {
+    Document document = new Document().append("keep", 1).append("drop", 2);
+
+    BSONMessageDecoder decoder = new BSONMessageDecoder();
+    decoder.init(Map.of(), Set.of("keep"), "testTopic");
+
+    GenericRow row = new GenericRow();
+    decoder.decode(encode(document), row);
+
+    assertEquals(row.getValue("keep"), 1);
+    assertNull(row.getValue("drop"));
+  }
+
+  @Test
+  public void testDecodeWithOffsetAndLength()
+      throws Exception {
+    Document document = new Document().append("k", "v");
+    byte[] encoded = encode(document);
+
+    // Embed the payload inside a larger buffer to exercise the offset/length 
decode path.
+    byte[] padded = new byte[encoded.length + 8];
+    int offset = 5;
+    System.arraycopy(encoded, 0, padded, offset, encoded.length);
+
+    BSONMessageDecoder decoder = new BSONMessageDecoder();
+    decoder.init(Map.of(), Set.of("k"), "testTopic");
+
+    GenericRow row = new GenericRow();
+    GenericRow result = decoder.decode(padded, offset, encoded.length, row);
+    assertSame(result, row);
+    assertEquals(row.getValue("k"), "v");
+  }
+
+  @Test(expectedExceptions = RuntimeException.class)
+  public void testDecodeCorruptPayloadThrows()
+      throws Exception {
+    BSONMessageDecoder decoder = new BSONMessageDecoder();
+    decoder.init(Map.of(), Set.of("k"), "testTopic");
+    // Not a valid framed BSON document: the decode failure is wrapped in a 
RuntimeException.
+    decoder.decode(new byte[]{1, 2, 3}, new GenericRow());
+  }
+
+  private static byte[] encode(Document document) {
+    DocumentCodec codec = new DocumentCodec();
+    BasicOutputBuffer buffer = new BasicOutputBuffer();
+    try (BsonBinaryWriter writer = new BsonBinaryWriter(buffer)) {
+      codec.encode(writer, document, EncoderContext.builder().build());
+    }
+    return buffer.toByteArray();
+  }
+}
diff --git 
a/pinot-plugins/pinot-input-format/pinot-bson/src/test/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordExtractorTest.java
 
b/pinot-plugins/pinot-input-format/pinot-bson/src/test/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordExtractorTest.java
new file mode 100644
index 00000000000..ae2ac2ce9e0
--- /dev/null
+++ 
b/pinot-plugins/pinot-input-format/pinot-bson/src/test/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordExtractorTest.java
@@ -0,0 +1,290 @@
+/**
+ * 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.pinot.plugin.inputformat.bson;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.bson.BsonBinaryWriter;
+import org.bson.BsonTimestamp;
+import org.bson.Document;
+import org.bson.codecs.DocumentCodec;
+import org.bson.codecs.EncoderContext;
+import org.bson.io.BasicOutputBuffer;
+import org.bson.types.Binary;
+import org.bson.types.Code;
+import org.bson.types.Decimal128;
+import org.bson.types.MaxKey;
+import org.bson.types.MinKey;
+import org.bson.types.ObjectId;
+import org.bson.types.Symbol;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+
+/// Tests [BSONRecordExtractor] — see its class Javadoc for the BSON type → 
Java output type matrix.
+public class BSONRecordExtractorTest {
+
+  private static final String COLUMN = "col";
+
+  // === Scalars — pass through ===
+
+  @Test
+  public void testBooleanPreserved() {
+    assertEquals(extract(true), true);
+    assertEquals(extract(false), false);
+  }
+
+  @Test
+  public void testIntegerPreserved() {
+    assertEquals(extract(42), 42);
+  }
+
+  @Test
+  public void testLongPreserved() {
+    assertEquals(extract(1_588_469_340_000L), 1_588_469_340_000L);
+  }
+
+  @Test
+  public void testDoublePreserved() {
+    assertEquals(extract(1.5d), 1.5d);
+  }
+
+  @Test
+  public void testStringPreserved() {
+    assertEquals(extract("hello"), "hello");
+  }
+
+  @Test
+  public void testNullPassedThrough() {
+    assertNull(extract(null));
+  }
+
+  // === Extended BSON types found in normal documents ===
+
+  @Test
+  public void testObjectIdConvertedToHexString() {
+    ObjectId objectId = new ObjectId("5f2b1c0e1c9d440000a1b2c3");
+    assertEquals(extract(objectId), "5f2b1c0e1c9d440000a1b2c3");
+  }
+
+  @Test
+  public void testDateConvertedToTimestamp() {
+    long epochMillis = 1_588_469_340_000L;
+    Object result = extract(new Date(epochMillis));
+    assertTrue(result instanceof Timestamp);
+    assertEquals(result, new Timestamp(epochMillis));
+  }
+
+  @Test
+  public void testDecimal128ConvertedToBigDecimal() {
+    assertEquals(extract(Decimal128.parse("123.456")), new 
BigDecimal("123.456"));
+  }
+
+  @Test
+  public void testDecimal128NaNConvertedToNull() {
+    // NaN / Infinity are legal Decimal128 values with no BigDecimal 
representation, so they surface as null.
+    assertNull(extract(Decimal128.parse("NaN")));
+    assertNull(extract(Decimal128.parse("Infinity")));
+    assertNull(extract(Decimal128.parse("-Infinity")));
+  }
+
+  @Test
+  public void testDecimal128NegativeZeroConvertedToZero() {
+    // Negative zero is a legal Decimal128 value that bigDecimalValue() 
rejects; it is numerically zero. It is
+    // negative-zero at any exponent, so "-0.00" must be handled too (it is 
not equal to Decimal128.NEGATIVE_ZERO).
+    assertEquals(extract(Decimal128.parse("-0")), BigDecimal.ZERO);
+    assertEquals(extract(Decimal128.parse("-0.00")), BigDecimal.ZERO);
+  }
+
+  @Test
+  public void testBinaryConvertedToByteArray() {
+    byte[] data = {1, 2, 3, 4};
+    Object result = extract(new Binary(data));
+    assertTrue(result instanceof byte[]);
+    assertEquals((byte[]) result, data);
+  }
+
+  @Test
+  public void testBsonTimestampConvertedToTimestamp() {
+    // The internal replication timestamp: the oplog `ts` field and the 
change-stream `clusterTime` field. Its
+    // seconds component becomes a java.sql.Timestamp; the intra-second 
ordinal counter is dropped.
+    Object result = extract(new BsonTimestamp(1_588_469_340, 7));
+    assertTrue(result instanceof Timestamp);
+    assertEquals(result, new Timestamp(1_588_469_340_000L));
+  }
+
+  @Test
+  public void testBsonTimestampAfter2038IsNotSignWrapped() {
+    // BSON stores the seconds component as an UNSIGNED 32-bit field, but 
BsonTimestamp#getTime() returns it as
+    // a signed int. From 2038-01-19T03:14:08Z onward the high bit is set, so 
a naive read wraps to a pre-1970
+    // date. 2039-01-01T00:00:00Z = 2_177_452_800 seconds, which does not fit 
a positive signed int.
+    long seconds = 2_177_452_800L;
+    BsonTimestamp afterY2038 = new BsonTimestamp((int) seconds, 1);
+    assertTrue(afterY2038.getTime() < 0, "precondition: the driver reports 
these seconds as a negative int");
+
+    Object result = extract(afterY2038);
+    assertEquals(result, new Timestamp(seconds * 1000L));
+  }
+
+  @Test
+  public void testUuidBinarySubtypesConvertedToRawBytes() {
+    // Subtypes 0x03 (legacy) and 0x04 (standard) are how MongoDB stores 
UUIDs. The standard DocumentCodec is
+    // built with UuidRepresentation.UNSPECIFIED, so it decodes them to Binary 
-- not java.util.UUID -- and they
+    // land as raw 16-byte BYTES. Round-trip through a real encode/decode so 
this pins the codec's behavior
+    // rather than our own hand-built Binary: if a driver upgrade flips the 
default representation, these
+    // columns would silently change from BYTES to a 36-char STRING.
+    UUID uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
+    byte[] raw = new byte[16];
+    
ByteBuffer.wrap(raw).putLong(uuid.getMostSignificantBits()).putLong(uuid.getLeastSignificantBits());
+
+    Document decoded = roundTrip(new Document()
+        .append("standard", new Binary((byte) 0x04, raw))
+        .append("legacy", new Binary((byte) 0x03, raw)));
+
+    GenericRow row = new GenericRow();
+    newExtractor().extract(decoded, row);
+    assertEquals((byte[]) row.getValue("standard"), raw);
+    assertEquals((byte[]) row.getValue("legacy"), raw);
+  }
+
+  @Test
+  public void testExoticTypesFallBackToPinnedToStringForms() {
+    // Rare / deprecated types with no Pinot-native representation fall back 
to value.toString(). Those strings
+    // are not a documented contract of org.mongodb:bson, and they get baked 
into segment data -- so pin them
+    // literally. A driver upgrade that reformats them must fail here rather 
than silently rewrite segments.
+    assertEquals(extract(new MaxKey()), "MaxKey");
+    assertEquals(extract(new MinKey()), "MinKey");
+    assertEquals(extract(new Symbol("sym")), "sym");
+    assertEquals(extract(new Code("function(){}")), 
"Code{code='function(){}'}");
+
+    // Regex and JS code survive an encode/decode round trip as 
BsonRegularExpression / Code.
+    Document decoded = roundTrip(new Document()
+        .append("regex", Pattern.compile("^a.*z$"))
+        .append("code", new Code("function(){}")));
+    GenericRow row = new GenericRow();
+    newExtractor().extract(decoded, row);
+    assertEquals(row.getValue("regex"), 
"BsonRegularExpression{pattern='^a.*z$', options=''}");
+    assertEquals(row.getValue("code"), "Code{code='function(){}'}");
+  }
+
+  // === Array (BSON array) → Object[] ===
+
+  @Test
+  public void testIntegerListExtractedAsArray() {
+    Object[] result = (Object[]) extract(List.of(10, 20, 30));
+    assertEquals(result, new Object[]{10, 20, 30});
+  }
+
+  @Test
+  public void testStringListExtractedAsArray() {
+    Object[] result = (Object[]) extract(List.of("foo", "bar"));
+    assertEquals(result, new Object[]{"foo", "bar"});
+  }
+
+  @Test
+  public void testListWithNullElement() {
+    Object[] result = (Object[]) extract(Arrays.asList(1, null, 3));
+    assertEquals(result, new Object[]{1, null, 3});
+  }
+
+  @Test
+  public void testEmptyListExtractedAsEmptyArray() {
+    Object[] result = (Object[]) extract(List.of());
+    assertEquals(result, new Object[]{});
+  }
+
+  @Test
+  public void testListOfObjectIdsConvertedElementwise() {
+    ObjectId id1 = new ObjectId("5f2b1c0e1c9d440000a1b2c3");
+    ObjectId id2 = new ObjectId("5f2b1c0e1c9d440000a1b2c4");
+    Object[] result = (Object[]) extract(List.of(id1, id2));
+    assertEquals(result, new Object[]{id1.toHexString(), id2.toHexString()});
+  }
+
+  // === Embedded document (BSON object) → Map<String, Object> ===
+
+  @Test
+  public void testEmbeddedDocumentConvertedToMap() {
+    Document embedded = new Document("k1", 1).append("k2", "foo").append("k3", 
true);
+    Map<?, ?> result = (Map<?, ?>) extract(embedded);
+    assertEquals(result.size(), 3);
+    assertEquals(result.get("k1"), 1);
+    assertEquals(result.get("k2"), "foo");
+    assertEquals(result.get("k3"), true);
+  }
+
+  @Test
+  public void testEmbeddedDocumentWithNullValue() {
+    Document embedded = new Document("present", 1);
+    embedded.put("absent", null);
+    Map<?, ?> result = (Map<?, ?>) extract(embedded);
+    assertEquals(result.get("present"), 1);
+    assertTrue(result.containsKey("absent"));
+    assertNull(result.get("absent"));
+  }
+
+  @Test
+  public void testNestedDocumentAndArray() {
+    Document embedded =
+        new Document("scalar", 1).append("nested", new Document("sub", 
1.1)).append("list", List.of("a", "b"));
+    Map<?, ?> result = (Map<?, ?>) extract(embedded);
+    assertEquals(result.get("scalar"), 1);
+    assertEquals(((Map<?, ?>) result.get("nested")).get("sub"), 1.1);
+    assertEquals((Object[]) result.get("list"), new Object[]{"a", "b"});
+  }
+
+  // === Helpers ===
+
+  /// Run the extractor against a single-column input document and return the 
extracted value.
+  private static Object extract(@Nullable Object input) {
+    Document record = new Document();
+    record.put(COLUMN, input);
+    GenericRow row = new GenericRow();
+    newExtractor().extract(record, row);
+    return row.getValue(COLUMN);
+  }
+
+  private static BSONRecordExtractor newExtractor() {
+    BSONRecordExtractor extractor = new BSONRecordExtractor();
+    extractor.init(null, null);
+    return extractor;
+  }
+
+  /// Encodes and re-decodes a document so the test sees the Java types the 
standard `DocumentCodec` actually
+  /// produces, rather than the ones the test handed in.
+  private static Document roundTrip(Document document) {
+    BasicOutputBuffer buffer = new BasicOutputBuffer();
+    try (BsonBinaryWriter writer = new BsonBinaryWriter(buffer)) {
+      new DocumentCodec().encode(writer, document, 
EncoderContext.builder().build());
+    }
+    return BSONUtils.decodeDocument(buffer.toByteArray());
+  }
+}
diff --git 
a/pinot-plugins/pinot-input-format/pinot-bson/src/test/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordReaderTest.java
 
b/pinot-plugins/pinot-input-format/pinot-bson/src/test/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordReaderTest.java
new file mode 100644
index 00000000000..c3b8bbc9d82
--- /dev/null
+++ 
b/pinot-plugins/pinot-input-format/pinot-bson/src/test/java/org/apache/pinot/plugin/inputformat/bson/BSONRecordReaderTest.java
@@ -0,0 +1,212 @@
+/**
+ * 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.pinot.plugin.inputformat.bson;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.FileUtils;
+import org.apache.pinot.spi.data.readers.AbstractRecordReaderTest;
+import org.apache.pinot.spi.data.readers.RecordReader;
+import org.bson.BsonBinaryWriter;
+import org.bson.Document;
+import org.bson.codecs.DocumentCodec;
+import org.bson.codecs.EncoderContext;
+import org.bson.io.BasicOutputBuffer;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.assertTrue;
+
+
+/// Tests [BSONRecordReader] against the shared [AbstractRecordReaderTest] 
contract (happy path + gzip + rewind
+/// over the generated schema), plus targeted negative / recovery cases driven 
by hand-built raw byte frames.
+///
+/// The generated `FLOAT` values are written as BSON doubles (BSON has no 
32-bit float type), so the reader
+/// reads them back as `Double`; the base assertions compare floating values 
with an epsilon, so this is
+/// transparent.
+public class BSONRecordReaderTest extends AbstractRecordReaderTest {
+
+  @Override
+  protected RecordReader createRecordReader(File file)
+      throws Exception {
+    BSONRecordReader recordReader = new BSONRecordReader();
+    recordReader.init(file, _sourceFields, null);
+    return recordReader;
+  }
+
+  @Override
+  protected void writeRecordsToFile(List<Map<String, Object>> recordsToWrite)
+      throws Exception {
+    try (OutputStream out = new BufferedOutputStream(new 
FileOutputStream(_dataFile))) {
+      for (Map<String, Object> record : recordsToWrite) {
+        Document document = new Document();
+        for (Map.Entry<String, Object> entry : record.entrySet()) {
+          document.put(entry.getKey(), toBsonValue(entry.getValue()));
+        }
+        out.write(encode(document));
+      }
+    }
+  }
+
+  /// BSON's codec has no float codec, so normalize Float (and Floats inside 
arrays) to Double before encoding.
+  private static Object toBsonValue(Object value) {
+    if (value instanceof Float) {
+      return ((Float) value).doubleValue();
+    }
+    if (value instanceof List) {
+      List<Object> converted = new ArrayList<>();
+      for (Object element : (List<?>) value) {
+        converted.add(toBsonValue(element));
+      }
+      return converted;
+    }
+    return value;
+  }
+
+  @Override
+  protected String getDataFileName() {
+    return "data.bson";
+  }
+
+  // === Negative / recovery cases over hand-built raw frames ===
+
+  @Test
+  public void testEmptyFileHasNoRecords()
+      throws Exception {
+    try (BSONRecordReader reader = readerOver("empty.bson", new byte[0])) {
+      assertFalse(reader.hasNext());
+    }
+  }
+
+  @Test
+  public void testTruncatedLengthPrefixThrows()
+      throws Exception {
+    // Only 2 of the 4 length-prefix bytes are present.
+    assertInitThrowsIOException("truncated-prefix.bson", new byte[]{0x0C, 
0x00});
+  }
+
+  @Test
+  public void testLengthBelowMinimumThrows()
+      throws Exception {
+    // Declared length 2 is below the 5-byte minimum for a BSON document.
+    assertInitThrowsIOException("invalid-length.bson", new byte[]{0x02, 0x00, 
0x00, 0x00});
+  }
+
+  @Test
+  public void testLengthAboveMaximumThrowsInsteadOfAllocating()
+      throws Exception {
+    // Declared length 0x7FFFFFFF (~2GB) exceeds the 16MB BSON maximum. It 
must be rejected before `new
+    // byte[length]` runs, otherwise an OutOfMemoryError (an Error) would 
escape the recoverable error paths.
+    assertInitThrowsIOException("oversized-length.bson", new byte[]{(byte) 
0xFF, (byte) 0xFF, (byte) 0xFF, 0x7F});
+  }
+
+  @Test
+  public void testTruncatedBodyThrows()
+      throws Exception {
+    // Declared length 16 but only 2 body bytes follow the prefix.
+    assertInitThrowsIOException("truncated-body.bson", new byte[]{0x10, 0x00, 
0x00, 0x00, 0x01, 0x02});
+  }
+
+  @Test
+  public void testRecoversAfterCorruptRecord()
+      throws Exception {
+    // A corrupt frame between two valid documents: length 8, then an unknown 
BSON element type (0x1F).
+    byte[] corruptFrame = {0x08, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00};
+    byte[] content =
+        concat(encode(new Document("a", 1)), corruptFrame, encode(new 
Document("b", 2)));
+    try (BSONRecordReader reader = readerOver("recovery.bson", content)) {
+      assertTrue(reader.hasNext());
+      assertEquals(reader.next().getValue("a"), 1);
+
+      // The corrupt frame surfaces as a skippable IOException; the reader 
then advances past it.
+      assertTrue(reader.hasNext());
+      assertThrows(IOException.class, reader::next);
+
+      // The trailing valid document is still returned.
+      assertTrue(reader.hasNext());
+      assertEquals(reader.next().getValue("b"), 2);
+      assertFalse(reader.hasNext());
+    }
+  }
+
+  @Test
+  public void testValidRecordEmittedBeforeTruncatedTail()
+      throws Exception {
+    // A complete document followed by a truncated frame (declares 16 bytes, 
only 2 body bytes present). The
+    // valid record is still emitted; the truncation surfaces on the following 
next() call.
+    byte[] truncatedFrame = {0x10, 0x00, 0x00, 0x00, 0x01, 0x02};
+    byte[] content = concat(encode(new Document("a", 1)), truncatedFrame);
+    try (BSONRecordReader reader = readerOver("truncated-tail.bson", content)) 
{
+      assertTrue(reader.hasNext());
+      assertEquals(reader.next().getValue("a"), 1);
+
+      assertTrue(reader.hasNext());
+      assertThrows(IOException.class, reader::next);
+      assertFalse(reader.hasNext());
+    }
+  }
+
+  private BSONRecordReader readerOver(String name, byte[] content)
+      throws IOException {
+    BSONRecordReader reader = new BSONRecordReader();
+    reader.init(writeTempFile(name, content), null, null);
+    return reader;
+  }
+
+  private void assertInitThrowsIOException(String name, byte[] content)
+      throws IOException {
+    File file = writeTempFile(name, content);
+    BSONRecordReader reader = new BSONRecordReader();
+    assertThrows(IOException.class, () -> reader.init(file, null, null));
+  }
+
+  private File writeTempFile(String name, byte[] content)
+      throws IOException {
+    File file = new File(_tempDir, name);
+    FileUtils.writeByteArrayToFile(file, content);
+    return file;
+  }
+
+  private static byte[] encode(Document document) {
+    DocumentCodec codec = new DocumentCodec();
+    BasicOutputBuffer buffer = new BasicOutputBuffer();
+    try (BsonBinaryWriter writer = new BsonBinaryWriter(buffer)) {
+      codec.encode(writer, document, EncoderContext.builder().build());
+    }
+    return buffer.toByteArray();
+  }
+
+  private static byte[] concat(byte[]... chunks)
+      throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    for (byte[] chunk : chunks) {
+      out.write(chunk);
+    }
+    return out.toByteArray();
+  }
+}
diff --git 
a/pinot-plugins/pinot-input-format/pinot-bson/src/test/resources/log4j2.xml 
b/pinot-plugins/pinot-input-format/pinot-bson/src/test/resources/log4j2.xml
new file mode 100644
index 00000000000..439331f9d7b
--- /dev/null
+++ b/pinot-plugins/pinot-input-format/pinot-bson/src/test/resources/log4j2.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<Configuration>
+  <Appenders>
+    <Console name="console" target="SYSTEM_OUT">
+      <PatternLayout pattern="%d{HH:mm:ss.SSS} %p [%c{1}] [%t] %m%n"/>
+    </Console>
+  </Appenders>
+  <Loggers>
+    <Logger name="org.apache.pinot" level="warn" additivity="false">
+      <AppenderRef ref="console"/>
+    </Logger>
+    <Root level="error">
+      <AppenderRef ref="console"/>
+    </Root>
+  </Loggers>
+</Configuration>
diff --git a/pinot-plugins/pinot-input-format/pom.xml 
b/pinot-plugins/pinot-input-format/pom.xml
index 43873ae79e8..57b453db90d 100644
--- a/pinot-plugins/pinot-input-format/pom.xml
+++ b/pinot-plugins/pinot-input-format/pom.xml
@@ -40,6 +40,7 @@
     <module>pinot-arrow</module>
     <module>pinot-avro</module>
     <module>pinot-avro-base</module>
+    <module>pinot-bson</module>
     <module>pinot-clp-log</module>
     <module>pinot-confluent-avro</module>
     <module>pinot-confluent-json</module>
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/data/readers/FileFormat.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/readers/FileFormat.java
index 588f7fe2b39..722e5d7a6af 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/data/readers/FileFormat.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/data/readers/FileFormat.java
@@ -19,7 +19,7 @@
 package org.apache.pinot.spi.data.readers;
 
 public enum FileFormat {
-  AVRO, GZIPPED_AVRO, CSV, JSON, PINOT, THRIFT, PARQUET, ORC, PROTO, ARROW, 
OTHER;
+  AVRO, GZIPPED_AVRO, CSV, JSON, PINOT, THRIFT, PARQUET, ORC, PROTO, ARROW, 
BSON, OTHER;
 
   /**
    * Converts an input format string to the corresponding FileFormat enum.
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/data/readers/RecordReaderFactory.java
 
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/readers/RecordReaderFactory.java
index 0500a9f4f15..a78730c44ae 100644
--- 
a/pinot-spi/src/main/java/org/apache/pinot/spi/data/readers/RecordReaderFactory.java
+++ 
b/pinot-spi/src/main/java/org/apache/pinot/spi/data/readers/RecordReaderFactory.java
@@ -44,6 +44,7 @@ public class RecordReaderFactory {
   static final String DEFAULT_CSV_RECORD_READER_CONFIG_CLASS =
       "org.apache.pinot.plugin.inputformat.csv.CSVRecordReaderConfig";
   static final String DEFAULT_JSON_RECORD_READER_CLASS = 
"org.apache.pinot.plugin.inputformat.json.JSONRecordReader";
+  static final String DEFAULT_BSON_RECORD_READER_CLASS = 
"org.apache.pinot.plugin.inputformat.bson.BSONRecordReader";
   static final String DEFAULT_THRIFT_RECORD_READER_CLASS =
       "org.apache.pinot.plugin.inputformat.thrift.ThriftRecordReader";
   static final String DEFAULT_THRIFT_RECORD_READER_CONFIG_CLASS =
@@ -76,6 +77,7 @@ public class RecordReaderFactory {
     register(FileFormat.GZIPPED_AVRO, DEFAULT_AVRO_RECORD_READER_CLASS, 
DEFAULT_AVRO_RECORD_READER_CONFIG_CLASS);
     register(FileFormat.CSV, DEFAULT_CSV_RECORD_READER_CLASS, 
DEFAULT_CSV_RECORD_READER_CONFIG_CLASS);
     register(FileFormat.JSON, DEFAULT_JSON_RECORD_READER_CLASS, null);
+    register(FileFormat.BSON, DEFAULT_BSON_RECORD_READER_CLASS, null);
     register(FileFormat.THRIFT, DEFAULT_THRIFT_RECORD_READER_CLASS, 
DEFAULT_THRIFT_RECORD_READER_CONFIG_CLASS);
     register(FileFormat.ORC, DEFAULT_ORC_RECORD_READER_CLASS, null);
     register(FileFormat.PARQUET, DEFAULT_PARQUET_RECORD_READER_CLASS, 
DEFAULT_PARQUET_RECORD_READER_CONFIG_CLASS);
diff --git 
a/pinot-spi/src/test/java/org/apache/pinot/spi/data/readers/RecordReaderFactoryTest.java
 
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/readers/RecordReaderFactoryTest.java
index 7c423727faf..657d7e9d115 100644
--- 
a/pinot-spi/src/test/java/org/apache/pinot/spi/data/readers/RecordReaderFactoryTest.java
+++ 
b/pinot-spi/src/test/java/org/apache/pinot/spi/data/readers/RecordReaderFactoryTest.java
@@ -34,6 +34,7 @@ public class RecordReaderFactoryTest {
     assertEquals(getRecordReaderClassName("gzipped_avro"), 
DEFAULT_AVRO_RECORD_READER_CLASS);
     assertEquals(getRecordReaderClassName("csv"), 
DEFAULT_CSV_RECORD_READER_CLASS);
     assertEquals(getRecordReaderClassName("json"), 
DEFAULT_JSON_RECORD_READER_CLASS);
+    assertEquals(getRecordReaderClassName("bson"), 
DEFAULT_BSON_RECORD_READER_CLASS);
     assertEquals(getRecordReaderClassName("thrift"), 
DEFAULT_THRIFT_RECORD_READER_CLASS);
     assertEquals(getRecordReaderClassName("orc"), 
DEFAULT_ORC_RECORD_READER_CLASS);
     assertEquals(getRecordReaderClassName("parquet"), 
DEFAULT_PARQUET_RECORD_READER_CLASS);
@@ -46,6 +47,7 @@ public class RecordReaderFactoryTest {
     assertEquals(getRecordReaderConfigClassName("gzipped_avro"), 
DEFAULT_AVRO_RECORD_READER_CONFIG_CLASS);
     assertEquals(getRecordReaderConfigClassName("csv"), 
DEFAULT_CSV_RECORD_READER_CONFIG_CLASS);
     assertNull(getRecordReaderConfigClassName("json"));
+    assertNull(getRecordReaderConfigClassName("bson"));
     assertEquals(getRecordReaderConfigClassName("thrift"), 
DEFAULT_THRIFT_RECORD_READER_CONFIG_CLASS);
     assertNull(getRecordReaderConfigClassName("orc"));
     assertEquals(getRecordReaderConfigClassName("parquet"), 
DEFAULT_PARQUET_RECORD_READER_CONFIG_CLASS);
@@ -115,6 +117,7 @@ public class RecordReaderFactoryTest {
     assertNotNull(DEFAULT_AVRO_RECORD_READER_CLASS);
     assertNotNull(DEFAULT_CSV_RECORD_READER_CLASS);
     assertNotNull(DEFAULT_JSON_RECORD_READER_CLASS);
+    assertNotNull(DEFAULT_BSON_RECORD_READER_CLASS);
     assertNotNull(DEFAULT_THRIFT_RECORD_READER_CLASS);
     assertNotNull(DEFAULT_ORC_RECORD_READER_CLASS);
     assertNotNull(DEFAULT_PARQUET_RECORD_READER_CLASS);
@@ -124,6 +127,7 @@ public class RecordReaderFactoryTest {
     assertEquals(DEFAULT_AVRO_RECORD_READER_CLASS, 
"org.apache.pinot.plugin.inputformat.avro.AvroRecordReader");
     assertEquals(DEFAULT_CSV_RECORD_READER_CLASS, 
"org.apache.pinot.plugin.inputformat.csv.CSVRecordReader");
     assertEquals(DEFAULT_JSON_RECORD_READER_CLASS, 
"org.apache.pinot.plugin.inputformat.json.JSONRecordReader");
+    assertEquals(DEFAULT_BSON_RECORD_READER_CLASS, 
"org.apache.pinot.plugin.inputformat.bson.BSONRecordReader");
     assertEquals(DEFAULT_PROTO_RECORD_READER_CLASS,
         "org.apache.pinot.plugin.inputformat.protobuf.ProtoBufRecordReader");
   }
diff --git a/pinot-tools/pom.xml b/pinot-tools/pom.xml
index 13d5887d0ff..7364716bc81 100644
--- a/pinot-tools/pom.xml
+++ b/pinot-tools/pom.xml
@@ -77,6 +77,10 @@
       <groupId>org.apache.pinot</groupId>
       <artifactId>pinot-json</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.apache.pinot</groupId>
+      <artifactId>pinot-bson</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.apache.pinot</groupId>
       <artifactId>pinot-orc</artifactId>
diff --git a/pom.xml b/pom.xml
index d52001301fe..d1a540996fc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -156,6 +156,7 @@
 
     <arrow.version>19.0.0</arrow.version>
     <avro.version>1.12.1</avro.version>
+    <bson.version>5.6.1</bson.version>
     <parquet.version>1.17.1</parquet.version>
     <orc.version>1.9.9</orc.version>
     <hive.version>2.8.1</hive.version>
@@ -777,6 +778,11 @@
         <artifactId>avro</artifactId>
         <version>${avro.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.mongodb</groupId>
+        <artifactId>bson</artifactId>
+        <version>${bson.version}</version>
+      </dependency>
       <dependency>
         <groupId>org.apache.avro</groupId>
         <artifactId>avro-mapred</artifactId>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to