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

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


The following commit(s) were added to refs/heads/master by this push:
     new 5ce27404e87 Filter all-null FIELD columns before tablet insert in 
Java/C++/Python… (#18533)
5ce27404e87 is described below

commit 5ce27404e872ca04ae5057fe0d1dd9f1fc3e6c6a
Author: Hongzhi Gao <[email protected]>
AuthorDate: Mon Aug 31 10:34:35 2026 +0800

    Filter all-null FIELD columns before tablet insert in Java/C++/Python… 
(#18533)
    
    * Filter all-null FIELD columns before tablet insert in Java/C++/Python 
clients
    
    Skip or shrink tablets that only contain null FIELD values so insert paths
    avoid shipping unused measurement columns across languages.
    
    * Keep TAG/ATTRIBUTE columns when all FIELD values are null in table model
    
    Table-model tablet inserts may carry only time and tag/attribute columns.
    Do not treat all-null FIELD columns as an empty tablet when non-FIELD
    columns remain, and clarify buildInsertTabletReq skip semantics.
    
    * Refactor C++ Tablet init and use metadata-only factory in 
filterNullColumns.
    
    Add a private constructor and createWithoutValueColumns to build tablets 
without pre-allocating value columns, fix bitmap null-check bounds, and update 
the Java table-model test constructor.
    
    * Check only active tablet rows when filtering all-null columns.
    
    BitMap is sized to maxRowNumber, so a full-map isAllMarked() can miss
    all-null FIELD columns when rowSize is smaller. Use isRangeAllMarked on
    [0, rowSize) in the Java, C++, and Python clients.
    
    * Keep null measurement names when filtering all-null tablet columns.
    
    A never-written column is treated as all-null; dropping it skipped the
    existing measurement-name check and omitted timeseries that tests expect.
---
 .../iotdb/session/it/IoTDBSessionSimpleIT.java     |   5 +-
 iotdb-client/client-cpp/src/include/Common.h       |   1 +
 iotdb-client/client-cpp/src/include/Session.h      |  50 +++---
 iotdb-client/client-cpp/src/rpc/SessionImpl.h      |   4 +-
 iotdb-client/client-cpp/src/session/Common.cpp     |  38 ++++-
 iotdb-client/client-cpp/src/session/Session.cpp    | 181 +++++++++++++++++----
 iotdb-client/client-cpp/test/CMakeLists.txt        |   7 +-
 .../client-cpp/test/cpp/sessionUtilsTest.cpp       | 145 +++++++++++++++++
 iotdb-client/client-cpp/test/main_utils.cpp        |  22 +++
 iotdb-client/client-py/iotdb/Session.py            | 102 ++++++++----
 iotdb-client/client-py/iotdb/utils/BitMap.py       |  39 +++++
 iotdb-client/client-py/iotdb/utils/SessionUtils.py | 128 +++++++++++++++
 iotdb-client/client-py/iotdb/utils/Tablet.py       |   6 +
 .../client-py/tests/unit/test_session_utils.py     | 160 ++++++++++++++++++
 .../java/org/apache/iotdb/session/Session.java     | 114 +++++++++++--
 .../apache/iotdb/session/util/SessionUtils.java    |  86 ++++++++++
 .../iotdb/session/util/SessionUtilsTest.java       |  83 ++++++++++
 17 files changed, 1053 insertions(+), 118 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBSessionSimpleIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBSessionSimpleIT.java
index 38f6345bae5..79a2225ca3b 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBSessionSimpleIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/session/it/IoTDBSessionSimpleIT.java
@@ -2164,6 +2164,7 @@ public class IoTDBSessionSimpleIT {
     tablet.addValue("s8", 0, new Binary(new byte[] {1}));
     tablet.addValue("s9", 0, "string_value");
     tablet.addValue("s10", 0, DateUtils.parseIntToLocalDate(20250403));
+    tablet.addValue("s11", 0, 1L);
 
     try (ISession session = EnvFactory.getEnv().getSessionConnection()) {
       session.insertTablet(tablet);
@@ -2193,8 +2194,8 @@ public class IoTDBSessionSimpleIT {
           assertEquals("string_value", iterator.getString("root.sg.d1.s9"));
           assertFalse(iterator.isNull("root.sg.d1.s10"));
           assertEquals(DateUtils.parseIntToLocalDate(20250403), 
iterator.getDate("root.sg.d1.s10"));
-          assertTrue(iterator.isNull("root.sg.d1.s11"));
-          assertNull(iterator.getTimestamp("root.sg.d1.s11"));
+          assertFalse(iterator.isNull("root.sg.d1.s11"));
+          assertEquals(new Timestamp(1), 
iterator.getTimestamp("root.sg.d1.s11"));
 
           assertEquals(new Timestamp(0), iterator.getTimestamp("Time"));
           assertFalse(iterator.isNull("Time"));
diff --git a/iotdb-client/client-cpp/src/include/Common.h 
b/iotdb-client/client-cpp/src/include/Common.h
index f36b38206b8..7a63c3f9341 100644
--- a/iotdb-client/client-cpp/src/include/Common.h
+++ b/iotdb-client/client-cpp/src/include/Common.h
@@ -246,6 +246,7 @@ public:
   bool isMarked(size_t position) const;
   bool isAllUnmarked() const;
   bool isAllMarked() const;
+  bool isRangeAllMarked(size_t start, size_t length) const;
   const std::vector<char>& getByteArray() const;
   size_t getSize() const;
 
diff --git a/iotdb-client/client-cpp/src/include/Session.h 
b/iotdb-client/client-cpp/src/include/Session.h
index a0910584e57..76895a30291 100644
--- a/iotdb-client/client-cpp/src/include/Session.h
+++ b/iotdb-client/client-cpp/src/include/Session.h
@@ -99,12 +99,21 @@ template <typename T, typename Target> void safe_cast(const 
T& value, Target& ta
  *
  */
 class Tablet {
+  friend class SessionUtils;
+
 private:
   static const int DEFAULT_ROW_SIZE = 1024;
 
   void createColumns();
   void deleteColumns();
 
+  struct WithoutValueColumnsTag {};
+
+  Tablet(const std::string& deviceId,
+         const std::vector<std::pair<std::string, TSDataType::TSDataType>>& 
schemas,
+         const std::vector<ColumnCategory> columnTypes, size_t maxRowNumber, 
bool isAligned,
+         WithoutValueColumnsTag);
+
 public:
   std::string deviceId; // deviceId of this tablet
   std::vector<std::pair<std::string, TSDataType::TSDataType>> schemas;
@@ -160,31 +169,18 @@ public:
          const std::vector<std::pair<std::string, TSDataType::TSDataType>>& 
schemas,
          const std::vector<ColumnCategory> columnTypes, size_t maxRowNumber,
          bool _isAligned = false)
-      : deviceId(deviceId), schemas(schemas), columnTypes(columnTypes), 
maxRowNumber(maxRowNumber),
-        isAligned(_isAligned) {
-    // create timestamp column
-    timestamps.resize(maxRowNumber);
-    // create value columns
-    values.resize(schemas.size());
+      : Tablet(deviceId, schemas, columnTypes, maxRowNumber, _isAligned, 
WithoutValueColumnsTag{}) {
     createColumns();
-    // init tagColumnIndexes
-    for (size_t i = 0; i < this->columnTypes.size(); i++) {
-      if (this->columnTypes[i] == ColumnCategory::TAG) {
-        tagColumnIndexes.push_back(i);
-      }
-    }
-    // create bitMaps
-    bitMaps.resize(schemas.size());
-    for (size_t i = 0; i < schemas.size(); i++) {
-      bitMaps[i].resize(maxRowNumber);
-    }
-    // create schemaNameIndex
-    for (size_t i = 0; i < schemas.size(); i++) {
-      schemaNameIndex[schemas[i].first] = i;
-    }
-    this->rowSize = 0;
   }
 
+  /**
+   * Create a tablet with metadata and bitmaps only; value columns are filled 
by the caller.
+   */
+  static std::shared_ptr<Tablet> createWithoutValueColumns(
+      const std::string& deviceId,
+      const std::vector<std::pair<std::string, TSDataType::TSDataType>>& 
schemas,
+      const std::vector<ColumnCategory>& columnTypes, size_t maxRowNumber, 
bool isAligned = false);
+
   Tablet(const Tablet& other)
       : deviceId(other.deviceId), schemas(other.schemas), 
schemaNameIndex(other.schemaNameIndex),
         columnTypes(other.columnTypes), timestamps(other.timestamps),
@@ -431,6 +427,16 @@ public:
   static std::string getValue(const Tablet& tablet);
 
   static bool isTabletContainsSingleDevice(Tablet tablet);
+
+  /**
+   * Drop entirely-null FIELD columns within [0, rowSize). TAG/ATTRIBUTE are 
kept.
+   * Does not mutate {@code tablet}.
+   *
+   * @return non-owning pointer to {@code tablet} if nothing to drop; a new 
tablet if filtered;
+   *         nullptr if no columns remain. Table-model TAG / ATTRIBUTE columns 
are kept even when
+   *         every FIELD column is null.
+   */
+  static std::shared_ptr<const Tablet> filterNullColumns(const Tablet& tablet);
 };
 
 class TemplateNode {
diff --git a/iotdb-client/client-cpp/src/rpc/SessionImpl.h 
b/iotdb-client/client-cpp/src/rpc/SessionImpl.h
index 9fc3d917293..406537486c0 100644
--- a/iotdb-client/client-cpp/src/rpc/SessionImpl.h
+++ b/iotdb-client/client-cpp/src/rpc/SessionImpl.h
@@ -145,7 +145,9 @@ public:
   void handleRedirection(const std::string& deviceId, TEndPoint endPoint);
   void handleRedirection(const std::shared_ptr<storage::IDeviceID>& deviceId, 
TEndPoint endPoint);
 
-  static void buildInsertTabletReq(TSInsertTabletReq& request, Tablet& tablet, 
bool sorted);
+  // Returns false when filtering leaves no columns (tree-model all-null FIELD 
tablet).
+  // Table-model inserts may keep TAG / ATTRIBUTE columns when all FIELD 
columns are null.
+  static bool buildInsertTabletReq(TSInsertTabletReq& request, Tablet& tablet, 
bool sorted);
   void insertTablet(TSInsertTabletReq request);
   void insertRelationalTabletOnce(
       const std::unordered_map<std::shared_ptr<SessionConnection>, Tablet>& 
relationalTabletGroup,
diff --git a/iotdb-client/client-cpp/src/session/Common.cpp 
b/iotdb-client/client-cpp/src/session/Common.cpp
index 859a8209a11..3dd829b95b0 100644
--- a/iotdb-client/client-cpp/src/session/Common.cpp
+++ b/iotdb-client/client-cpp/src/session/Common.cpp
@@ -323,18 +323,40 @@ bool BitMap::isAllUnmarked() const {
 }
 
 bool BitMap::isAllMarked() const {
-  size_t j;
-  for (j = 0; j < size >> 3; j++) {
-    if (bits[j] != (char)0XFF) {
-      return false;
-    }
+  return isRangeAllMarked(0, size);
+}
+
+bool BitMap::isRangeAllMarked(size_t start, size_t length) const {
+  // size_t addition can wrap; short-circuit so `size - start` is only used 
when
+  // start <= size. Out-of-range matches mark()/isMarked(): return false.
+  if (start > size || length > size - start) {
+    return false;
   }
-  for (j = 0; j < size % 8; j++) {
-    if ((bits[size >> 3] & ((char)1 << j)) == 0) {
+  if (length == 0) {
+    return true;
+  }
+
+  const size_t end = start + length;
+  const size_t firstByte = start >> 3;
+  const size_t lastByte = (end - 1) >> 3;
+  if (firstByte == lastByte) {
+    const unsigned char mask = static_cast<unsigned char>(((1U << length) - 
1U) << (start & 7U));
+    return (static_cast<unsigned char>(bits[firstByte]) & mask) == mask;
+  }
+
+  const unsigned char firstMask = static_cast<unsigned char>((0xFFU << (start 
& 7U)) & 0xFFU);
+  if ((static_cast<unsigned char>(bits[firstByte]) & firstMask) != firstMask) {
+    return false;
+  }
+  for (size_t index = firstByte + 1; index < lastByte; index++) {
+    if (static_cast<unsigned char>(bits[index]) != 0xFFU) {
       return false;
     }
   }
-  return true;
+  const size_t lastBitCount = end & 7U;
+  const unsigned char lastMask =
+      lastBitCount == 0 ? 0xFFU : static_cast<unsigned char>((1U << 
lastBitCount) - 1U);
+  return (static_cast<unsigned char>(bits[lastByte]) & lastMask) == lastMask;
 }
 
 const std::vector<char>& BitMap::getByteArray() const {
diff --git a/iotdb-client/client-cpp/src/session/Session.cpp 
b/iotdb-client/client-cpp/src/session/Session.cpp
index cf429af0c4d..ab71732ebe1 100644
--- a/iotdb-client/client-cpp/src/session/Session.cpp
+++ b/iotdb-client/client-cpp/src/session/Session.cpp
@@ -69,6 +69,39 @@ TSDataType::TSDataType getTSDataTypeFromString(const string& 
str) {
   return TSDataType::UNKNOWN;
 }
 
+Tablet::Tablet(const std::string& deviceId,
+               const std::vector<std::pair<std::string, 
TSDataType::TSDataType>>& schemas,
+               const std::vector<ColumnCategory> columnTypes, size_t 
maxRowNumber, bool isAligned,
+               WithoutValueColumnsTag)
+    : deviceId(deviceId), schemas(schemas), columnTypes(columnTypes), 
maxRowNumber(maxRowNumber),
+      isAligned(isAligned) {
+  timestamps.resize(maxRowNumber);
+  values.resize(schemas.size(), nullptr);
+  tagColumnIndexes.clear();
+  for (size_t i = 0; i < columnTypes.size(); i++) {
+    if (columnTypes[i] == ColumnCategory::TAG) {
+      tagColumnIndexes.push_back(static_cast<int>(i));
+    }
+  }
+  bitMaps.resize(schemas.size());
+  for (size_t i = 0; i < schemas.size(); i++) {
+    bitMaps[i].resize(maxRowNumber);
+  }
+  schemaNameIndex.clear();
+  for (size_t i = 0; i < schemas.size(); i++) {
+    schemaNameIndex[schemas[i].first] = i;
+  }
+  rowSize = 0;
+}
+
+std::shared_ptr<Tablet> Tablet::createWithoutValueColumns(
+    const std::string& deviceId,
+    const std::vector<std::pair<std::string, TSDataType::TSDataType>>& schemas,
+    const std::vector<ColumnCategory>& columnTypes, size_t maxRowNumber, bool 
isAligned) {
+  return std::shared_ptr<Tablet>(new Tablet(deviceId, schemas, columnTypes, 
maxRowNumber, isAligned,
+                                            WithoutValueColumnsTag{}));
+}
+
 void Tablet::createColumns() {
   for (size_t i = 0; i < schemas.size(); i++) {
     TSDataType::TSDataType dataType = schemas[i].second;
@@ -413,6 +446,64 @@ bool SessionUtils::isTabletContainsSingleDevice(Tablet 
tablet) {
   return true;
 }
 
+static bool isColumnAllNull(const BitMap& bitMap, size_t rowSize) {
+  if (rowSize == 0) {
+    return false;
+  }
+  // BitMap is sized to maxRowNumber; only [0, rowSize) are active rows.
+  return bitMap.isRangeAllMarked(0, rowSize);
+}
+
+std::shared_ptr<const Tablet> SessionUtils::filterNullColumns(const Tablet& 
tablet) {
+  const size_t columnCount = tablet.schemas.size();
+  if (columnCount == 0 || tablet.bitMaps.size() < columnCount) {
+    return std::shared_ptr<const Tablet>(&tablet, [](const Tablet*) {});
+  }
+
+  std::vector<size_t> keptIndices;
+  keptIndices.reserve(columnCount);
+
+  for (size_t i = 0; i < columnCount; i++) {
+    ColumnCategory category =
+        i < tablet.columnTypes.size() ? tablet.columnTypes[i] : 
ColumnCategory::FIELD;
+    bool isField = category == ColumnCategory::FIELD;
+    bool drop = isField && isColumnAllNull(tablet.bitMaps[i], tablet.rowSize);
+    if (drop) {
+      continue;
+    }
+    keptIndices.push_back(i);
+  }
+
+  if (keptIndices.size() == columnCount) {
+    return std::shared_ptr<const Tablet>(&tablet, [](const Tablet*) {});
+  }
+  if (keptIndices.empty()) {
+    return nullptr;
+  }
+
+  std::vector<std::pair<std::string, TSDataType::TSDataType>> keptSchemas;
+  std::vector<ColumnCategory> keptColumnTypes;
+  keptSchemas.reserve(keptIndices.size());
+  keptColumnTypes.reserve(keptIndices.size());
+  for (size_t idx : keptIndices) {
+    keptSchemas.push_back(tablet.schemas[idx]);
+    keptColumnTypes.push_back(idx < tablet.columnTypes.size() ? 
tablet.columnTypes[idx]
+                                                              : 
ColumnCategory::FIELD);
+  }
+
+  auto filteredOut = Tablet::createWithoutValueColumns(
+      tablet.deviceId, keptSchemas, keptColumnTypes, tablet.maxRowNumber, 
tablet.isAligned);
+  filteredOut->timestamps = tablet.timestamps;
+  filteredOut->rowSize = tablet.rowSize;
+  for (size_t ni = 0; ni < keptIndices.size(); ni++) {
+    size_t oi = keptIndices[ni];
+    Tablet::deepCopyTabletColValue(&tablet.values[oi], 
&filteredOut->values[ni],
+                                   keptSchemas[ni].second, 
static_cast<int>(tablet.maxRowNumber));
+    filteredOut->bitMaps[ni] = tablet.bitMaps[oi];
+  }
+  return filteredOut;
+}
+
 string MeasurementNode::serialize() const {
   MyStringBuffer buffer;
   buffer.putString(getName());
@@ -1002,6 +1093,10 @@ void 
Session::Impl::insertTabletsWithLeaderCache(unordered_map<string, Tablet*>&
     }
     auto deviceId = item.first;
     auto tablet = item.second;
+    std::shared_ptr<const Tablet> toEncode = 
SessionUtils::filterNullColumns(*tablet);
+    if (!toEncode) {
+      continue;
+    }
     auto connection = getSessionConnection(deviceId);
     auto it = tabletsGroup.find(connection);
     if (it == tabletsGroup.end()) {
@@ -1009,13 +1104,13 @@ void 
Session::Impl::insertTabletsWithLeaderCache(unordered_map<string, Tablet*>&
       tabletsGroup[connection] = request;
     }
     TSInsertTabletsReq& existingReq = tabletsGroup[connection];
-    existingReq.prefixPaths.emplace_back(tablet->deviceId);
-    
existingReq.timestampsList.emplace_back(move(SessionUtils::getTime(*tablet)));
-    existingReq.valuesList.emplace_back(move(SessionUtils::getValue(*tablet)));
-    existingReq.sizeList.emplace_back(tablet->rowSize);
+    existingReq.prefixPaths.emplace_back(toEncode->deviceId);
+    
existingReq.timestampsList.emplace_back(move(SessionUtils::getTime(*toEncode)));
+    
existingReq.valuesList.emplace_back(move(SessionUtils::getValue(*toEncode)));
+    existingReq.sizeList.emplace_back(toEncode->rowSize);
     vector<int> dataTypes;
     vector<string> measurements;
-    for (pair<string, TSDataType::TSDataType> schema : tablet->schemas) {
+    for (pair<string, TSDataType::TSDataType> schema : toEncode->schemas) {
       measurements.push_back(schema.first);
       dataTypes.push_back(schema.second);
     }
@@ -1023,6 +1118,10 @@ void 
Session::Impl::insertTabletsWithLeaderCache(unordered_map<string, Tablet*>&
     existingReq.typesList.emplace_back(dataTypes);
   }
 
+  if (tabletsGroup.empty()) {
+    return;
+  }
+
   std::function<void(std::shared_ptr<SessionConnection>, const 
TSInsertTabletsReq&)> consumer =
       [](const std::shared_ptr<SessionConnection>& c, const 
TSInsertTabletsReq& r) {
         c->insertTablets(r);
@@ -1444,27 +1543,41 @@ void Session::insertTablet(Tablet& tablet) {
   }
 }
 
-void Session::Impl::buildInsertTabletReq(TSInsertTabletReq& request, Tablet& 
tablet, bool sorted) {
+bool Session::Impl::buildInsertTabletReq(TSInsertTabletReq& request, Tablet& 
tablet, bool sorted) {
   if ((!sorted) && !checkSorted(tablet)) {
     sortTablet(tablet);
   }
 
-  request.__set_prefixPath(tablet.deviceId);
+  std::shared_ptr<const Tablet> toEncode = 
SessionUtils::filterNullColumns(tablet);
+  if (!toEncode) {
+    return false;
+  }
+
+  request.__set_prefixPath(toEncode->deviceId);
 
   std::vector<std::string> reqMeasurements;
-  reqMeasurements.reserve(tablet.schemas.size());
+  reqMeasurements.reserve(toEncode->schemas.size());
   std::vector<int32_t> types;
-  types.reserve(tablet.schemas.size());
-  for (pair<string, TSDataType::TSDataType> schema : tablet.schemas) {
+  types.reserve(toEncode->schemas.size());
+  for (pair<string, TSDataType::TSDataType> schema : toEncode->schemas) {
     reqMeasurements.push_back(schema.first);
     types.push_back(schema.second);
   }
   request.__set_measurements(reqMeasurements);
   request.__set_types(types);
-  request.__set_values(SessionUtils::getValue(tablet));
-  request.__set_timestamps(SessionUtils::getTime(tablet));
-  request.__set_size(tablet.rowSize);
-  request.__set_isAligned(tablet.isAligned);
+  request.__set_values(SessionUtils::getValue(*toEncode));
+  request.__set_timestamps(SessionUtils::getTime(*toEncode));
+  request.__set_size(toEncode->rowSize);
+  request.__set_isAligned(toEncode->isAligned);
+  if (!toEncode->columnTypes.empty()) {
+    std::vector<int8_t> columnCategories;
+    columnCategories.reserve(toEncode->columnTypes.size());
+    for (auto& category : toEncode->columnTypes) {
+      columnCategories.push_back(static_cast<int8_t>(category));
+    }
+    request.__set_columnCategories(columnCategories);
+  }
+  return true;
 }
 
 void Session::Impl::insertTablet(TSInsertTabletReq request) {
@@ -1488,7 +1601,9 @@ void Session::Impl::insertTablet(TSInsertTabletReq 
request) {
 
 void Session::insertTablet(Tablet& tablet, bool sorted) {
   TSInsertTabletReq request;
-  impl_->buildInsertTabletReq(request, tablet, sorted);
+  if (!impl_->buildInsertTabletReq(request, tablet, sorted)) {
+    return;
+  }
   impl_->insertTablet(request);
 }
 
@@ -1574,13 +1689,10 @@ void Session::Impl::insertRelationalTabletOnce(
   auto connection = iter->first;
   auto tablet = iter->second;
   TSInsertTabletReq request;
-  buildInsertTabletReq(request, tablet, sorted);
-  request.__set_writeToTable(true);
-  std::vector<int8_t> columnCategories;
-  for (auto& category : tablet.columnTypes) {
-    columnCategories.push_back(static_cast<int8_t>(category));
+  if (!buildInsertTabletReq(request, tablet, sorted)) {
+    return;
   }
-  request.__set_columnCategories(columnCategories);
+  request.__set_writeToTable(true);
   try {
     TSStatus respStatus;
     connection->getSessionClient()->insertTablet(respStatus, request);
@@ -1629,14 +1741,10 @@ void Session::Impl::insertRelationalTabletByGroup(
     futures.emplace_back(
         std::async(std::launch::async, [this, connection, tablet, sorted]() 
mutable {
           TSInsertTabletReq request;
-          buildInsertTabletReq(request, tablet, sorted);
-          request.__set_writeToTable(true);
-
-          std::vector<int8_t> columnCategories;
-          for (auto& category : tablet.columnTypes) {
-            columnCategories.push_back(static_cast<int8_t>(category));
+          if (!buildInsertTabletReq(request, tablet, sorted)) {
+            return;
           }
-          request.__set_columnCategories(columnCategories);
+          request.__set_writeToTable(true);
 
           try {
             TSStatus respStatus;
@@ -1702,18 +1810,25 @@ void Session::insertTablets(unordered_map<string, 
Tablet*>& tablets, bool sorted
       if (!impl_->checkSorted(*(item.second))) {
         impl_->sortTablet(*(item.second));
       }
-      request.prefixPaths.push_back(item.second->deviceId);
+      std::shared_ptr<const Tablet> toEncode = 
SessionUtils::filterNullColumns(*(item.second));
+      if (!toEncode) {
+        continue;
+      }
+      request.prefixPaths.push_back(toEncode->deviceId);
       vector<string> measurements;
       vector<int> dataTypes;
-      for (pair<string, TSDataType::TSDataType> schema : item.second->schemas) 
{
+      for (pair<string, TSDataType::TSDataType> schema : toEncode->schemas) {
         measurements.push_back(schema.first);
         dataTypes.push_back(schema.second);
       }
       request.measurementsList.push_back(measurements);
       request.typesList.push_back(dataTypes);
-      
request.timestampsList.push_back(move(SessionUtils::getTime(*(item.second))));
-      
request.valuesList.push_back(move(SessionUtils::getValue(*(item.second))));
-      request.sizeList.push_back(item.second->rowSize);
+      request.timestampsList.push_back(move(SessionUtils::getTime(*toEncode)));
+      request.valuesList.push_back(move(SessionUtils::getValue(*toEncode)));
+      request.sizeList.push_back(toEncode->rowSize);
+    }
+    if (request.prefixPaths.empty()) {
+      return;
     }
     request.__set_isAligned(isAligned);
     try {
diff --git a/iotdb-client/client-cpp/test/CMakeLists.txt 
b/iotdb-client/client-cpp/test/CMakeLists.txt
index 9d5428edc4b..b4a7cf1f767 100644
--- a/iotdb-client/client-cpp/test/CMakeLists.txt
+++ b/iotdb-client/client-cpp/test/CMakeLists.txt
@@ -42,12 +42,14 @@ set(_test_targets
         session_tests
         session_relational_tests
         session_c_tests
-        session_c_relational_tests)
+        session_c_relational_tests
+        session_utils_tests)
 
 add_executable(session_tests              main.cpp              
cpp/sessionIT.cpp)
 add_executable(session_relational_tests   main_Relational.cpp   
cpp/sessionRelationalIT.cpp)
 add_executable(session_c_tests            main_c.cpp            
cpp/sessionCIT.cpp)
 add_executable(session_c_relational_tests main_c_Relational.cpp 
cpp/sessionCRelationalIT.cpp)
+add_executable(session_utils_tests        main_utils.cpp        
cpp/sessionUtilsTest.cpp)
 
 foreach(_t IN LISTS _test_targets)
     target_include_directories(${_t} PRIVATE
@@ -86,6 +88,7 @@ if(MSVC)
     add_test(NAME sessionRelationalIT    CONFIGURATIONS Release COMMAND 
session_relational_tests)
     add_test(NAME sessionCIT             CONFIGURATIONS Release COMMAND 
session_c_tests)
     add_test(NAME sessionCRelationalIT   CONFIGURATIONS Release COMMAND 
session_c_relational_tests)
+    add_test(NAME sessionUtilsTest       CONFIGURATIONS Release COMMAND 
session_utils_tests)
     foreach(_t IN LISTS _test_targets)
         add_custom_command(TARGET ${_t} POST_BUILD
                 COMMAND ${CMAKE_COMMAND} -E copy_if_different
@@ -96,9 +99,11 @@ else()
     add_test(NAME sessionRelationalIT    COMMAND session_relational_tests)
     add_test(NAME sessionCIT             COMMAND session_c_tests)
     add_test(NAME sessionCRelationalIT   COMMAND session_c_relational_tests)
+    add_test(NAME sessionUtilsTest       COMMAND session_utils_tests)
 endif()
 
 # Run sequentially: parallel ctest overloads the single local IoTDB instance.
+# sessionUtilsTest is a pure unit test and can run anytime.
 set_tests_properties(
         sessionIT sessionRelationalIT sessionCIT sessionCRelationalIT
         PROPERTIES RUN_SERIAL TRUE)
diff --git a/iotdb-client/client-cpp/test/cpp/sessionUtilsTest.cpp 
b/iotdb-client/client-cpp/test/cpp/sessionUtilsTest.cpp
new file mode 100644
index 00000000000..18047a38868
--- /dev/null
+++ b/iotdb-client/client-cpp/test/cpp/sessionUtilsTest.cpp
@@ -0,0 +1,145 @@
+/**
+ * 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.
+ */
+
+#include "catch.hpp"
+#include "Session.h"
+
+#include <memory>
+#include <vector>
+
+using namespace std;
+
+TEST_CASE("SessionUtils filterNullColumns keeps only non-null FIELD columns", 
"[utils]") {
+  vector<pair<string, TSDataType::TSDataType>> schemas = {{"s1", 
TSDataType::INT32},
+                                                          {"s2", 
TSDataType::INT64},
+                                                          {"s3", 
TSDataType::FLOAT},
+                                                          {"s4", 
TSDataType::DOUBLE},
+                                                          {"s5", 
TSDataType::BOOLEAN}};
+  Tablet tablet("root.sg.d1", schemas, 1);
+  tablet.timestamps[0] = 1000L;
+  tablet.rowSize = 1;
+  tablet.addValue("s1", 0, 1);
+  tablet.addValue("s3", 0, 1.5f);
+  tablet.bitMaps[1].mark(0);
+  tablet.bitMaps[3].mark(0);
+  tablet.bitMaps[4].mark(0);
+
+  std::shared_ptr<const Tablet> filtered = 
SessionUtils::filterNullColumns(tablet);
+  REQUIRE(filtered != nullptr);
+  REQUIRE(filtered.get() != &tablet);
+  REQUIRE(filtered.use_count() == 1);
+  REQUIRE(filtered->schemas.size() == 2);
+  REQUIRE(filtered->schemas[0].first == "s1");
+  REQUIRE(filtered->schemas[1].first == "s3");
+  REQUIRE(((int*)filtered->values[0])[0] == 1);
+  REQUIRE(((float*)filtered->values[1])[0] == Approx(1.5f));
+}
+
+TEST_CASE("SessionUtils filterNullColumns returns original when nothing to 
drop", "[utils]") {
+  vector<pair<string, TSDataType::TSDataType>> schemas = {{"s1", 
TSDataType::INT32}};
+  Tablet tablet("root.sg.d1", schemas, 1);
+  tablet.timestamps[0] = 1L;
+  tablet.rowSize = 1;
+  tablet.addValue("s1", 0, 1);
+
+  std::shared_ptr<const Tablet> filtered = 
SessionUtils::filterNullColumns(tablet);
+  REQUIRE(filtered.get() == &tablet);
+}
+
+TEST_CASE("SessionUtils filterNullColumns returns nullptr when tree-model 
FIELD columns are all null",
+          "[utils]") {
+  vector<pair<string, TSDataType::TSDataType>> schemas = {{"s1", 
TSDataType::INT32},
+                                                          {"s2", 
TSDataType::INT64}};
+  Tablet tablet("root.sg.d1", schemas, 1);
+  tablet.timestamps[0] = 2000L;
+  tablet.rowSize = 1;
+  tablet.bitMaps[0].mark(0);
+  tablet.bitMaps[1].mark(0);
+
+  std::shared_ptr<const Tablet> filtered = 
SessionUtils::filterNullColumns(tablet);
+  REQUIRE(filtered == nullptr);
+}
+
+TEST_CASE("SessionUtils filterNullColumns keeps TAG when table-model FIELD 
columns are all null",
+          "[utils]") {
+  vector<pair<string, TSDataType::TSDataType>> schemas = {
+      {"tag1", TSDataType::TEXT}, {"s1", TSDataType::INT32}, {"s2", 
TSDataType::INT64}};
+  vector<ColumnCategory> columnTypes = {ColumnCategory::TAG, 
ColumnCategory::FIELD,
+                                        ColumnCategory::FIELD};
+  Tablet tablet("table1", schemas, columnTypes, 1);
+  tablet.timestamps[0] = 3000L;
+  tablet.rowSize = 1;
+  tablet.addValue("tag1", 0, string("d1"));
+  tablet.bitMaps[1].mark(0);
+  tablet.bitMaps[2].mark(0);
+
+  std::shared_ptr<const Tablet> filtered = 
SessionUtils::filterNullColumns(tablet);
+  REQUIRE(filtered != nullptr);
+  REQUIRE(filtered.get() != &tablet);
+  REQUIRE(filtered->schemas.size() == 1);
+  REQUIRE(filtered->schemas[0].first == "tag1");
+  REQUIRE(filtered->columnTypes.size() == 1);
+  REQUIRE(filtered->columnTypes[0] == ColumnCategory::TAG);
+}
+
+TEST_CASE("SessionUtils filterNullColumns checks active rows when bitmap is 
maxRowNumber-sized",
+          "[utils]") {
+  vector<pair<string, TSDataType::TSDataType>> schemas = {{"s1", 
TSDataType::INT32},
+                                                          {"s2", 
TSDataType::INT64}};
+  Tablet tablet("root.sg.d1", schemas, 10);
+  tablet.timestamps[0] = 1000L;
+  tablet.rowSize = 1;
+  tablet.addValue("s1", 0, 1);
+  tablet.bitMaps[1].mark(0);
+
+  std::shared_ptr<const Tablet> filtered = 
SessionUtils::filterNullColumns(tablet);
+  REQUIRE(filtered != nullptr);
+  REQUIRE(filtered->schemas.size() == 1);
+  REQUIRE(filtered->schemas[0].first == "s1");
+}
+
+TEST_CASE("BitMap isRangeAllMarked matches per-bit scan and rejects OOB", 
"[utils]") {
+  BitMap bitMap(20);
+  for (size_t i = 0; i < 20; i++) {
+    if (i % 4 != 2) {
+      bitMap.mark(i);
+    }
+  }
+  for (size_t start = 0; start <= 20; start++) {
+    for (size_t length = 0; length <= 20 - start; length++) {
+      bool allMarked = true;
+      for (size_t i = start; i < start + length; i++) {
+        allMarked = allMarked && bitMap.isMarked(i);
+      }
+      REQUIRE(bitMap.isRangeAllMarked(start, length) == allMarked);
+    }
+  }
+  REQUIRE(bitMap.isRangeAllMarked(0, 0));
+  REQUIRE_FALSE(bitMap.isRangeAllMarked(0, 21));
+  REQUIRE_FALSE(bitMap.isRangeAllMarked(21, 0));
+
+  BitMap empty;
+  REQUIRE(empty.isAllMarked());
+  BitMap full(8);
+  full.markAll();
+  REQUIRE(full.isAllMarked());
+  full.unmark(7);
+  REQUIRE_FALSE(full.isAllMarked());
+  REQUIRE(full.isRangeAllMarked(0, 7));
+}
diff --git a/iotdb-client/client-cpp/test/main_utils.cpp 
b/iotdb-client/client-cpp/test/main_utils.cpp
new file mode 100644
index 00000000000..3c9c54084cb
--- /dev/null
+++ b/iotdb-client/client-cpp/test/main_utils.cpp
@@ -0,0 +1,22 @@
+/**
+ * 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.
+ */
+
+#define CATCH_CONFIG_MAIN
+
+#include <catch.hpp>
diff --git a/iotdb-client/client-py/iotdb/Session.py 
b/iotdb-client/client-py/iotdb/Session.py
index b962e3ead5e..5a4f2ff23e7 100644
--- a/iotdb-client/client-py/iotdb/Session.py
+++ b/iotdb-client/client-py/iotdb/Session.py
@@ -24,6 +24,7 @@ import sys
 import warnings
 
 from iotdb.utils.SessionDataSet import SessionDataSet
+from iotdb.utils.SessionUtils import filter_null_columns
 from thrift.protocol import TBinaryProtocol, TCompactProtocol
 from thrift.transport import TSocket, TTransport
 from tzlocal import get_localzone_name
@@ -1095,6 +1096,8 @@ class Session(object):
         :param tablet: a tablet specified above
         """
         request = self.gen_insert_tablet_req(tablet)
+        if request is None:
+            return
         try:
             connection = self.get_connection(tablet.get_insert_target_name())
             request.sessionId = connection.session_id
@@ -1123,19 +1126,24 @@ class Session(object):
         if self.__enable_redirection:
             request_group = {}
             for i in range(len(tablet_lst)):
-                connection = 
self.get_connection(tablet_lst[i].get_insert_target_name())
+                filtered = filter_null_columns(tablet_lst[i])
+                if filtered is None:
+                    continue
+                connection = 
self.get_connection(filtered.get_insert_target_name())
                 request = request_group.setdefault(
                     connection.client,
                     TSInsertTabletsReq(
                         connection.session_id, [], [], [], [], [], [], False
                     ),
                 )
-                
request.prefixPaths.append(tablet_lst[i].get_insert_target_name())
-                
request.timestampsList.append(tablet_lst[i].get_binary_timestamps())
-                
request.measurementsList.append(tablet_lst[i].get_measurements())
-                request.valuesList.append(tablet_lst[i].get_binary_values())
-                request.sizeList.append(tablet_lst[i].get_row_number())
-                request.typesList.append(tablet_lst[i].get_data_types())
+                request.prefixPaths.append(filtered.get_insert_target_name())
+                request.timestampsList.append(filtered.get_binary_timestamps())
+                request.measurementsList.append(filtered.get_measurements())
+                request.valuesList.append(filtered.get_binary_values())
+                request.sizeList.append(filtered.get_row_number())
+                request.typesList.append(filtered.get_data_types())
+            if not request_group:
+                return 0
             for client, request in request_group.items():
                 try:
                     
rpc_utils.verify_success_with_redirection_for_multi_devices(
@@ -1161,6 +1169,8 @@ class Session(object):
             return 0
         else:
             request = self.gen_insert_tablets_req(tablet_lst)
+            if request is None:
+                return 0
             try:
                 return 
rpc_utils.verify_success(self.__client.insertTablets(request))
             except TTransport.TException as e:
@@ -1190,6 +1200,8 @@ class Session(object):
         :param tablet: a tablet specified above
         """
         request = self.gen_insert_tablet_req(tablet, True)
+        if request is None:
+            return
         try:
             connection = self.get_connection(tablet.get_insert_target_name())
             request.sessionId = connection.session_id
@@ -1218,19 +1230,24 @@ class Session(object):
         if self.__enable_redirection:
             request_group = {}
             for i in range(len(tablet_lst)):
-                connection = 
self.get_connection(tablet_lst[i].get_insert_target_name())
+                filtered = filter_null_columns(tablet_lst[i])
+                if filtered is None:
+                    continue
+                connection = 
self.get_connection(filtered.get_insert_target_name())
                 request = request_group.setdefault(
                     connection.client,
                     TSInsertTabletsReq(
                         connection.session_id, [], [], [], [], [], [], True
                     ),
                 )
-                
request.prefixPaths.append(tablet_lst[i].get_insert_target_name())
-                
request.timestampsList.append(tablet_lst[i].get_binary_timestamps())
-                
request.measurementsList.append(tablet_lst[i].get_measurements())
-                request.valuesList.append(tablet_lst[i].get_binary_values())
-                request.sizeList.append(tablet_lst[i].get_row_number())
-                request.typesList.append(tablet_lst[i].get_data_types())
+                request.prefixPaths.append(filtered.get_insert_target_name())
+                request.timestampsList.append(filtered.get_binary_timestamps())
+                request.measurementsList.append(filtered.get_measurements())
+                request.valuesList.append(filtered.get_binary_values())
+                request.sizeList.append(filtered.get_row_number())
+                request.typesList.append(filtered.get_data_types())
+            if not request_group:
+                return 0
             for client, request in request_group.items():
                 try:
                     
rpc_utils.verify_success_with_redirection_for_multi_devices(
@@ -1256,6 +1273,8 @@ class Session(object):
             return 0
         else:
             request = self.gen_insert_tablets_req(tablet_lst, True)
+            if request is None:
+                return 0
             try:
                 return 
rpc_utils.verify_success(self.__client.insertTablets(request))
             except TTransport.TException as e:
@@ -1282,6 +1301,8 @@ class Session(object):
         :param tablet: a tablet specified above
         """
         request = self.gen_insert_relational_tablet_req(tablet)
+        if request is None:
+            return
         try:
             connection = self.get_connection(tablet.get_insert_target_name())
             request.sessionId = connection.session_id
@@ -1480,6 +1501,8 @@ class Session(object):
         :param tablet: a tablet of data
         """
         request = self.gen_insert_tablet_req(tablet)
+        if request is None:
+            return
         try:
             return 
rpc_utils.verify_success(self.__client.testInsertTablet(request))
         except TTransport.TException as e:
@@ -1501,6 +1524,8 @@ class Session(object):
         :param tablet_list: List of tablets
         """
         request = self.gen_insert_tablets_req(tablet_list)
+        if request is None:
+            return
         try:
             return 
rpc_utils.verify_success(self.__client.testInsertTablets(request))
         except TTransport.TException as e:
@@ -1516,29 +1541,35 @@ class Session(object):
                 raise IoTDBConnectionException(self.connection_error_msg()) 
from None
 
     def gen_insert_tablet_req(self, tablet, is_aligned=False):
+        filtered = filter_null_columns(tablet)
+        if filtered is None:
+            return None
         return TSInsertTabletReq(
             self.__session_id,
-            tablet.get_insert_target_name(),
-            tablet.get_measurements(),
-            tablet.get_binary_values(),
-            tablet.get_binary_timestamps(),
-            tablet.get_data_types(),
-            tablet.get_row_number(),
+            filtered.get_insert_target_name(),
+            filtered.get_measurements(),
+            filtered.get_binary_values(),
+            filtered.get_binary_timestamps(),
+            filtered.get_data_types(),
+            filtered.get_row_number(),
             is_aligned,
         )
 
     def gen_insert_relational_tablet_req(self, tablet, is_aligned=False):
+        filtered = filter_null_columns(tablet)
+        if filtered is None:
+            return None
         return TSInsertTabletReq(
             self.__session_id,
-            tablet.get_insert_target_name(),
-            tablet.get_measurements(),
-            tablet.get_binary_values(),
-            tablet.get_binary_timestamps(),
-            tablet.get_data_types(),
-            tablet.get_row_number(),
+            filtered.get_insert_target_name(),
+            filtered.get_measurements(),
+            filtered.get_binary_values(),
+            filtered.get_binary_timestamps(),
+            filtered.get_data_types(),
+            filtered.get_row_number(),
             is_aligned,
             True,
-            tablet.get_column_categories(),
+            filtered.get_column_categories(),
         )
 
     def gen_insert_tablets_req(self, tablet_lst, is_aligned=False):
@@ -1549,12 +1580,17 @@ class Session(object):
         type_lst = []
         size_lst = []
         for tablet in tablet_lst:
-            device_id_lst.append(tablet.get_insert_target_name())
-            measurements_lst.append(tablet.get_measurements())
-            values_lst.append(tablet.get_binary_values())
-            timestamps_lst.append(tablet.get_binary_timestamps())
-            type_lst.append(tablet.get_data_types())
-            size_lst.append(tablet.get_row_number())
+            filtered = filter_null_columns(tablet)
+            if filtered is None:
+                continue
+            device_id_lst.append(filtered.get_insert_target_name())
+            measurements_lst.append(filtered.get_measurements())
+            values_lst.append(filtered.get_binary_values())
+            timestamps_lst.append(filtered.get_binary_timestamps())
+            type_lst.append(filtered.get_data_types())
+            size_lst.append(filtered.get_row_number())
+        if not device_id_lst:
+            return None
         return TSInsertTabletsReq(
             self.__session_id,
             device_id_lst,
diff --git a/iotdb-client/client-py/iotdb/utils/BitMap.py 
b/iotdb-client/client-py/iotdb/utils/BitMap.py
index 621bf6c7df9..d0b57ed1383 100644
--- a/iotdb-client/client-py/iotdb/utils/BitMap.py
+++ b/iotdb-client/client-py/iotdb/utils/BitMap.py
@@ -29,6 +29,12 @@ class BitMap(object):
     def mark(self, position):
         self.bits[position // 8] |= BitMap.BIT_UTIL[position % 8]
 
+    def is_marked(self, position):
+        return (self.bits[position // 8] & BitMap.BIT_UTIL[position % 8]) != 0
+
+    def get_size(self):
+        return self.__size
+
     def is_all_unmarked(self):
         for i in range(self.__size // 8):
             if self.bits[i] != 0:
@@ -37,3 +43,36 @@ class BitMap(object):
             if (self.bits[self.__size // 8] & BitMap.BIT_UTIL[i]) != 0:
                 return False
         return True
+
+    def is_all_marked(self):
+        return self.is_range_all_marked(0, self.__size)
+
+    def is_range_all_marked(self, start, length):
+        # Reject negatives: Python // on a negative start would index bits from
+        # the tail (bits[-1]). Out-of-range matches C++ BitMap: return False.
+        if (
+            start < 0
+            or length < 0
+            or start > self.__size
+            or length > self.__size - start
+        ):
+            return False
+        if length == 0:
+            return True
+
+        end = start + length
+        first_byte = start // 8
+        last_byte = (end - 1) // 8
+        if first_byte == last_byte:
+            mask = ((1 << length) - 1) << (start & 7)
+            return (self.bits[first_byte] & mask) == mask
+
+        first_mask = (0xFF << (start & 7)) & 0xFF
+        if (self.bits[first_byte] & first_mask) != first_mask:
+            return False
+        for index in range(first_byte + 1, last_byte):
+            if self.bits[index] != 0xFF:
+                return False
+        last_bit_count = end & 7
+        last_mask = 0xFF if last_bit_count == 0 else (1 << last_bit_count) - 1
+        return (self.bits[last_byte] & last_mask) == last_mask
diff --git a/iotdb-client/client-py/iotdb/utils/SessionUtils.py 
b/iotdb-client/client-py/iotdb/utils/SessionUtils.py
new file mode 100644
index 00000000000..eaf64e6fea0
--- /dev/null
+++ b/iotdb-client/client-py/iotdb/utils/SessionUtils.py
@@ -0,0 +1,128 @@
+# 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.
+#
+
+from typing import List, Optional, Union
+
+from iotdb.utils.BitMap import BitMap
+from iotdb.utils.NumpyTablet import NumpyTablet
+from iotdb.utils.Tablet import ColumnType, Tablet
+
+
+def _is_column_all_null_bitmap(bitmap: Optional[BitMap], row_number: int) -> 
bool:
+    if bitmap is None or row_number <= 0:
+        return False
+    # BitMap is sized to maxRowNumber; only [0, row_number) are active rows.
+    return bitmap.is_range_all_marked(0, row_number)
+
+
+def _is_tablet_column_all_null(tablet: Tablet, column_index: int) -> bool:
+    values = tablet.get_values()
+    for row in range(tablet.get_row_number()):
+        if values[row][column_index] is not None:
+            return False
+    return tablet.get_row_number() > 0
+
+
+def filter_null_columns(
+    tablet: Union[Tablet, NumpyTablet],
+) -> Optional[Union[Tablet, NumpyTablet]]:
+    """
+    Drop entirely-null FIELD columns. TAG/ATTRIBUTE are always kept.
+    Does not mutate the input tablet.
+
+    Returns:
+        the same instance if nothing to drop;
+        a new tablet with remaining columns;
+        None if no columns remain. Table-model TAG / ATTRIBUTE columns are 
kept even when
+        every FIELD column is null.
+    """
+    if tablet is None:
+        return None
+
+    column_types = tablet.get_column_categories()
+    column_number = len(tablet.get_measurements())
+    row_number = tablet.get_row_number()
+
+    kept_indices: List[int] = []
+
+    is_numpy = isinstance(tablet, NumpyTablet)
+    bitmaps = tablet.bitmaps if is_numpy else None
+
+    for i in range(column_number):
+        category = (
+            column_types[i]
+            if column_types is not None and i < len(column_types)
+            else ColumnType.FIELD
+        )
+        is_field = category == ColumnType.FIELD
+
+        if is_field:
+            if is_numpy:
+                bitmap = (
+                    bitmaps[i] if bitmaps is not None and i < len(bitmaps) 
else None
+                )
+                drop = _is_column_all_null_bitmap(bitmap, row_number)
+            else:
+                drop = _is_tablet_column_all_null(tablet, i)
+        else:
+            drop = False
+
+        if drop:
+            continue
+
+        kept_indices.append(i)
+
+    if len(kept_indices) == column_number:
+        return tablet
+    if not kept_indices:
+        return None
+
+    measurements = [tablet.get_measurements()[i] for i in kept_indices]
+    data_types = [tablet.get_data_types()[i] for i in kept_indices]
+    kept_column_types = [column_types[i] for i in kept_indices]
+
+    if is_numpy:
+        values = [tablet.get_values()[i] for i in kept_indices]
+        kept_bitmaps = None
+        if bitmaps is not None:
+            kept_bitmaps = [
+                bitmaps[i] if i < len(bitmaps) else None for i in kept_indices
+            ]
+        return NumpyTablet(
+            tablet.get_insert_target_name(),
+            measurements,
+            data_types,
+            values,
+            tablet.get_timestamps(),
+            bitmaps=kept_bitmaps,
+            column_types=kept_column_types,
+        )
+
+    # Tablet stores row-oriented values
+    src_values = tablet.get_values()
+    values = []
+    for row in range(row_number):
+        values.append([src_values[row][i] for i in kept_indices])
+    return Tablet(
+        tablet.get_insert_target_name(),
+        measurements,
+        data_types,
+        values,
+        list(tablet.get_timestamps()),
+        column_types=kept_column_types,
+    )
diff --git a/iotdb-client/client-py/iotdb/utils/Tablet.py 
b/iotdb-client/client-py/iotdb/utils/Tablet.py
index 9b241723fe5..1239972486b 100644
--- a/iotdb-client/client-py/iotdb/utils/Tablet.py
+++ b/iotdb-client/client-py/iotdb/utils/Tablet.py
@@ -115,6 +115,12 @@ class Tablet(object):
     def get_insert_target_name(self):
         return self.__insert_target_name
 
+    def get_timestamps(self):
+        return self.__timestamps
+
+    def get_values(self):
+        return self.__values
+
     def get_binary_timestamps(self):
         format_str_list = [">"]
         values_tobe_packed = []
diff --git a/iotdb-client/client-py/tests/unit/test_session_utils.py 
b/iotdb-client/client-py/tests/unit/test_session_utils.py
new file mode 100644
index 00000000000..0af54b74320
--- /dev/null
+++ b/iotdb-client/client-py/tests/unit/test_session_utils.py
@@ -0,0 +1,160 @@
+# 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.
+#
+
+import numpy as np
+
+from iotdb.utils.BitMap import BitMap
+from iotdb.utils.IoTDBConstants import TSDataType
+from iotdb.utils.NumpyTablet import NumpyTablet
+from iotdb.utils.SessionUtils import filter_null_columns
+from iotdb.utils.Tablet import ColumnType, Tablet
+
+
+def test_filter_null_columns_tablet():
+    measurements = ["s1", "s2", "s3", "s4", "s5"]
+    data_types = [
+        TSDataType.INT32,
+        TSDataType.INT64,
+        TSDataType.FLOAT,
+        TSDataType.DOUBLE,
+        TSDataType.BOOLEAN,
+    ]
+    values = [[1, None, 1.5, None, None]]
+    timestamps = [1000]
+    tablet = Tablet("root.sg.d1", measurements, data_types, values, timestamps)
+
+    filtered = filter_null_columns(tablet)
+    assert filtered is not None
+    assert filtered is not tablet
+    assert filtered.get_measurements() == ["s1", "s3"]
+    assert filtered.get_values()[0] == [1, 1.5]
+
+    # nothing to drop
+    dense = Tablet(
+        "root.sg.d1",
+        ["s1"],
+        [TSDataType.INT32],
+        [[1]],
+        [1],
+    )
+    assert filter_null_columns(dense) is dense
+
+    # all null
+    all_null = Tablet(
+        "root.sg.d1",
+        measurements,
+        data_types,
+        [[None, None, None, None, None]],
+        [2000],
+    )
+    assert filter_null_columns(all_null) is None
+
+
+def test_filter_null_columns_table_model_keeps_tag():
+    tablet = Tablet(
+        "table1",
+        ["tag1", "s1", "s2"],
+        [TSDataType.STRING, TSDataType.INT32, TSDataType.INT32],
+        [["d1", None, None]],
+        [3000],
+        column_types=[ColumnType.TAG, ColumnType.FIELD, ColumnType.FIELD],
+    )
+    filtered = filter_null_columns(tablet)
+    assert filtered is not None
+    assert filtered is not tablet
+    assert filtered.get_measurements() == ["tag1"]
+    assert filtered.get_column_categories() == [ColumnType.TAG]
+
+
+def test_filter_null_columns_numpy_tablet():
+    measurements = ["s1", "s2", "s3"]
+    data_types = [TSDataType.INT32, TSDataType.INT64, TSDataType.FLOAT]
+    values = [
+        np.array([1], dtype=np.dtype(">i4")),
+        np.array([0], dtype=np.dtype(">i8")),
+        np.array([1.5], dtype=np.dtype(">f4")),
+    ]
+    timestamps = np.array([1000], dtype=np.dtype(">i8"))
+    bitmaps = [BitMap(1), BitMap(1), BitMap(1)]
+    bitmaps[1].mark(0)
+
+    np_tablet = NumpyTablet(
+        "root.sg.d1",
+        measurements,
+        data_types,
+        values,
+        timestamps,
+        bitmaps=bitmaps,
+    )
+    filtered = filter_null_columns(np_tablet)
+    assert filtered is not None
+    assert filtered is not np_tablet
+    assert filtered.get_measurements() == ["s1", "s3"]
+
+
+def test_filter_null_columns_numpy_tablet_active_rows():
+    measurements = ["s1", "s2"]
+    data_types = [TSDataType.INT32, TSDataType.INT64]
+    values = [
+        np.array([1], dtype=np.dtype(">i4")),
+        np.array([0], dtype=np.dtype(">i8")),
+    ]
+    timestamps = np.array([1000], dtype=np.dtype(">i8"))
+    bitmaps = [BitMap(10), BitMap(10)]
+    bitmaps[1].mark(0)
+
+    np_tablet = NumpyTablet(
+        "root.sg.d1",
+        measurements,
+        data_types,
+        values,
+        timestamps,
+        bitmaps=bitmaps,
+    )
+    filtered = filter_null_columns(np_tablet)
+    assert filtered is not None
+    assert filtered is not np_tablet
+    assert filtered.get_measurements() == ["s1"]
+
+
+def test_bitmap_is_range_all_marked():
+    bit_map = BitMap(20)
+    for i in range(20):
+        if i % 4 != 2:
+            bit_map.mark(i)
+    for start in range(21):
+        for length in range(21 - start):
+            all_marked = all(bit_map.is_marked(i) for i in range(start, start 
+ length))
+            assert bit_map.is_range_all_marked(start, length) is all_marked
+    assert bit_map.is_range_all_marked(0, 0) is True
+    assert bit_map.is_range_all_marked(0, 21) is False
+    assert bit_map.is_range_all_marked(21, 0) is False
+    assert bit_map.is_range_all_marked(-1, 1) is False
+    assert bit_map.is_range_all_marked(0, -1) is False
+
+    empty = BitMap(0)
+    assert empty.is_all_marked() is True
+    full = BitMap(8)
+    for i in range(8):
+        full.mark(i)
+    assert full.is_all_marked() is True
+    partial = BitMap(8)
+    for i in range(7):
+        partial.mark(i)
+    assert partial.is_all_marked() is False
+    assert partial.is_range_all_marked(0, 7) is True
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
index 36581b9690c..39bb9c06cba 100644
--- a/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
+++ b/iotdb-client/session/src/main/java/org/apache/iotdb/session/Session.java
@@ -2806,6 +2806,14 @@ public class Session implements ISession {
   public void insertTablet(Tablet tablet, boolean sorted)
       throws IoTDBConnectionException, StatementExecutionException {
     TSInsertTabletReq request = genTSInsertTabletReq(tablet, sorted, false);
+    if (request == null) {
+      logger.warn(
+          ALL_VALUES_ARE_NULL,
+          tablet.getDeviceId(),
+          tablet.getRowSize() > 0 ? tablet.getTimestamp(0) : null,
+          tablet.getSchemas());
+      return;
+    }
     insertTabletInternal(tablet, request);
   }
 
@@ -2849,8 +2857,15 @@ public class Session implements ISession {
       insertRelationalTabletWithLeaderCache(tablet);
     } else {
       TSInsertTabletReq request = genTSInsertTabletReq(tablet, false, false);
+      if (request == null) {
+        logger.warn(
+            ALL_VALUES_ARE_NULL,
+            tablet.getDeviceId(),
+            tablet.getRowSize() > 0 ? tablet.getTimestamp(0) : null,
+            tablet.getSchemas());
+        return;
+      }
       request.setWriteToTable(true);
-      
request.setColumnCategories(toEnumOrdinalsAsBytes(tablet.getColumnTypes()));
       try {
         getDefaultSessionConnection().insertTablet(request);
       } catch (RedirectException ignored) {
@@ -2915,8 +2930,15 @@ public class Session implements ISession {
     SessionConnection connection = entry.getKey();
     Tablet tablet = entry.getValue();
     TSInsertTabletReq request = genTSInsertTabletReq(tablet, false, false);
+    if (request == null) {
+      logger.warn(
+          ALL_VALUES_ARE_NULL,
+          tablet.getDeviceId(),
+          tablet.getRowSize() > 0 ? tablet.getTimestamp(0) : null,
+          tablet.getSchemas());
+      return;
+    }
     request.setWriteToTable(true);
-    
request.setColumnCategories(toEnumOrdinalsAsBytes(tablet.getColumnTypes()));
     try {
       connection.insertTablet(request);
     } catch (RedirectException e) {
@@ -2956,9 +2978,15 @@ public class Session implements ISession {
                   return CompletableFuture.runAsync(
                       () -> {
                         TSInsertTabletReq request = 
genTSInsertTabletReq(subTablet, false, false);
+                        if (request == null) {
+                          logger.warn(
+                              ALL_VALUES_ARE_NULL,
+                              subTablet.getDeviceId(),
+                              subTablet.getRowSize() > 0 ? 
subTablet.getTimestamp(0) : null,
+                              subTablet.getSchemas());
+                          return;
+                        }
                         request.setWriteToTable(true);
-                        request.setColumnCategories(
-                            toEnumOrdinalsAsBytes(subTablet.getColumnTypes()));
                         InsertConsumer<TSInsertTabletReq> insertConsumer =
                             SessionConnection::insertTablet;
                         try {
@@ -3037,6 +3065,14 @@ public class Session implements ISession {
   public void insertAlignedTablet(Tablet tablet, boolean sorted)
       throws IoTDBConnectionException, StatementExecutionException {
     TSInsertTabletReq request = genTSInsertTabletReq(tablet, sorted, true);
+    if (request == null) {
+      logger.warn(
+          ALL_VALUES_ARE_NULL,
+          tablet.getDeviceId(),
+          tablet.getRowSize() > 0 ? tablet.getTimestamp(0) : null,
+          tablet.getSchemas());
+      return;
+    }
     try {
       getSessionConnection(tablet.getDeviceId()).insertTablet(request);
     } catch (RedirectException e) {
@@ -3065,9 +3101,14 @@ public class Session implements ISession {
       sortTablet(tablet);
     }
 
+    Tablet filtered = SessionUtils.filterNullColumns(tablet);
+    if (filtered == null) {
+      return null;
+    }
+
     TSInsertTabletReq request = new TSInsertTabletReq();
 
-    for (IMeasurementSchema measurementSchema : tablet.getSchemas()) {
+    for (IMeasurementSchema measurementSchema : filtered.getSchemas()) {
       if (measurementSchema.getMeasurementName() == null) {
         throw new 
IllegalArgumentException(SessionMessages.MEASUREMENT_NON_NULL);
       }
@@ -3075,22 +3116,25 @@ public class Session implements ISession {
       request.addToTypes(measurementSchema.getType().ordinal());
     }
 
-    request.setPrefixPath(tablet.getDeviceId());
+    request.setPrefixPath(filtered.getDeviceId());
     request.setIsAligned(isAligned);
+    if (filtered.getColumnTypes() != null) {
+      
request.setColumnCategories(toEnumOrdinalsAsBytes(filtered.getColumnTypes()));
+    }
 
     boolean trulyEnableRpcCompression =
-        enableIoTDBRpcCompression && tablet.getRowSize() >= 
tabletCompressionMinRowSize;
+        enableIoTDBRpcCompression && filtered.getRowSize() >= 
tabletCompressionMinRowSize;
 
     List<Byte> encodingTypes;
     if (trulyEnableRpcCompression) {
-      encodingTypes = new ArrayList<>(tablet.getSchemas().size() + 1);
+      encodingTypes = new ArrayList<>(filtered.getSchemas().size() + 1);
       encodingTypes.add(
           this.columnEncodersMap
               .getOrDefault(
                   TSDataType.INT64,
                   
TSEncoding.valueOf(TSFileDescriptor.getInstance().getConfig().getTimeEncoder()))
               .serialize());
-      for (IMeasurementSchema measurementSchema : tablet.getSchemas()) {
+      for (IMeasurementSchema measurementSchema : filtered.getSchemas()) {
         if (measurementSchema.getMeasurementName() == null) {
           throw new 
IllegalArgumentException(SessionMessages.MEASUREMENT_NON_NULL);
         }
@@ -3105,7 +3149,7 @@ public class Session implements ISession {
       }
     } else {
       encodingTypes =
-          Collections.nCopies(tablet.getSchemas().size() + 1, 
TSEncoding.PLAIN.serialize());
+          Collections.nCopies(filtered.getSchemas().size() + 1, 
TSEncoding.PLAIN.serialize());
     }
 
     TabletEncoder encoder =
@@ -3118,10 +3162,10 @@ public class Session implements ISession {
       request.setCompressType(compressionType.serialize());
       request.setEncodingTypes(encodingTypes);
     }
-    request.setTimestamps(encoder.encodeTime(tablet));
-    request.setValues(encoder.encodeValues(tablet));
+    request.setTimestamps(encoder.encodeTime(filtered));
+    request.setValues(encoder.encodeValues(filtered));
 
-    request.setSize(tablet.getRowSize());
+    request.setSize(filtered.getRowSize());
     return request;
   }
 
@@ -3154,6 +3198,9 @@ public class Session implements ISession {
     } else {
       TSInsertTabletsReq request =
           genTSInsertTabletsReq(new ArrayList<>(tablets.values()), sorted, 
false);
+      if (request == null) {
+        return;
+      }
       try {
         getDefaultSessionConnection().insertTablets(request);
       } catch (RedirectException ignored) {
@@ -3190,6 +3237,9 @@ public class Session implements ISession {
     } else {
       TSInsertTabletsReq request =
           genTSInsertTabletsReq(new ArrayList<>(tablets.values()), sorted, 
true);
+      if (request == null) {
+        return;
+      }
       try {
         getDefaultSessionConnection().insertTablets(request);
       } catch (RedirectException ignored) {
@@ -3208,6 +3258,11 @@ public class Session implements ISession {
       updateTSInsertTabletsReq(request, entry.getValue(), sorted, isAligned);
     }
 
+    tabletGroup.entrySet().removeIf(e -> e.getValue().getPrefixPathsSize() == 
0);
+    if (tabletGroup.isEmpty()) {
+      return;
+    }
+
     if (tabletGroup.size() == 1) {
       insertOnce(tabletGroup, SessionConnection::insertTablets);
     } else {
@@ -3224,6 +3279,9 @@ public class Session implements ISession {
     for (Tablet tablet : tablets) {
       updateTSInsertTabletsReq(request, tablet, sorted, isAligned);
     }
+    if (request.getPrefixPathsSize() == 0) {
+      return null;
+    }
     return request;
   }
 
@@ -3232,11 +3290,20 @@ public class Session implements ISession {
     if (!checkSorted(tablet)) {
       sortTablet(tablet);
     }
-    request.addToPrefixPaths(tablet.getDeviceId());
+    Tablet filtered = SessionUtils.filterNullColumns(tablet);
+    if (filtered == null) {
+      logger.warn(
+          ALL_VALUES_ARE_NULL,
+          tablet.getDeviceId(),
+          tablet.getRowSize() > 0 ? tablet.getTimestamp(0) : null,
+          tablet.getSchemas());
+      return;
+    }
+    request.addToPrefixPaths(filtered.getDeviceId());
     List<String> measurements = new ArrayList<>();
     List<Integer> dataTypes = new ArrayList<>();
     request.setIsAligned(isAligned);
-    for (IMeasurementSchema measurementSchema : tablet.getSchemas()) {
+    for (IMeasurementSchema measurementSchema : filtered.getSchemas()) {
       if (measurementSchema.getMeasurementName() == null) {
         throw new 
IllegalArgumentException(SessionMessages.MEASUREMENT_NON_NULL);
       }
@@ -3245,9 +3312,9 @@ public class Session implements ISession {
     }
     request.addToMeasurementsList(measurements);
     request.addToTypesList(dataTypes);
-    request.addToTimestampsList(SessionUtils.getTimeBuffer(tablet));
-    request.addToValuesList(SessionUtils.getValueBuffer(tablet));
-    request.addToSizeList(tablet.getRowSize());
+    request.addToTimestampsList(SessionUtils.getTimeBuffer(filtered));
+    request.addToValuesList(SessionUtils.getValueBuffer(filtered));
+    request.addToSizeList(filtered.getRowSize());
   }
 
   // sample some records and judge whether need to add too many null values to 
convert to tablet.
@@ -3465,6 +3532,14 @@ public class Session implements ISession {
   public void testInsertTablet(Tablet tablet, boolean sorted)
       throws IoTDBConnectionException, StatementExecutionException {
     TSInsertTabletReq request = genTSInsertTabletReq(tablet, sorted, false);
+    if (request == null) {
+      logger.warn(
+          ALL_VALUES_ARE_NULL,
+          tablet.getDeviceId(),
+          tablet.getRowSize() > 0 ? tablet.getTimestamp(0) : null,
+          tablet.getSchemas());
+      return;
+    }
     getDefaultSessionConnection().testInsertTablet(request);
   }
 
@@ -3487,6 +3562,9 @@ public class Session implements ISession {
       throws IoTDBConnectionException, StatementExecutionException {
     TSInsertTabletsReq request =
         genTSInsertTabletsReq(new ArrayList<>(tablets.values()), sorted, 
false);
+    if (request == null) {
+      return;
+    }
     getDefaultSessionConnection().testInsertTablets(request);
   }
 
diff --git 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/util/SessionUtils.java
 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/util/SessionUtils.java
index 26a2e46e59d..b3b29f0be93 100644
--- 
a/iotdb-client/session/src/main/java/org/apache/iotdb/session/util/SessionUtils.java
+++ 
b/iotdb-client/session/src/main/java/org/apache/iotdb/session/util/SessionUtils.java
@@ -26,6 +26,7 @@ import org.apache.iotdb.session.i18n.SessionMessages;
 
 import org.apache.tsfile.common.conf.TSFileConfig;
 import org.apache.tsfile.encoding.encoder.Encoder;
+import org.apache.tsfile.enums.ColumnCategory;
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.file.metadata.IDeviceID;
 import org.apache.tsfile.utils.Binary;
@@ -467,6 +468,91 @@ public class SessionUtils {
     }
   }
 
+  /**
+   * Remove FIELD columns that are entirely null within {@code [0, rowSize)} 
according to BitMap.
+   * TAG / ATTRIBUTE columns are always kept. Does not mutate the input tablet.
+   *
+   * @param tablet source tablet
+   * @return the same instance if nothing to drop; a new tablet with remaining 
columns; or {@code
+   *     null} if no columns remain (e.g. tree-model tablet whose FIELD 
columns are all null). For
+   *     table-model tablets, TAG / ATTRIBUTE columns are kept even when every 
FIELD column is null.
+   */
+  public static Tablet filterNullColumns(Tablet tablet) {
+    if (tablet == null) {
+      return null;
+    }
+    BitMap[] bitMaps = tablet.getBitMaps();
+    if (bitMaps == null) {
+      return tablet;
+    }
+
+    List<IMeasurementSchema> schemas = tablet.getSchemas();
+    List<ColumnCategory> columnCategories = tablet.getColumnTypes();
+    Object[] values = tablet.getValues();
+    int columnCount = schemas.size();
+    int rowSize = tablet.getRowSize();
+
+    List<IMeasurementSchema> keptSchemas = new ArrayList<>(columnCount);
+    List<ColumnCategory> keptCategories =
+        columnCategories != null ? new ArrayList<>(columnCount) : null;
+    List<Object> keptValues = new ArrayList<>(columnCount);
+    List<BitMap> keptBitMaps = new ArrayList<>(columnCount);
+
+    for (int i = 0; i < columnCount; i++) {
+      ColumnCategory category =
+          columnCategories != null && i < columnCategories.size()
+              ? columnCategories.get(i)
+              : ColumnCategory.FIELD;
+      boolean isField = category == ColumnCategory.FIELD;
+
+      boolean drop =
+          isField
+              && schemas.get(i).getMeasurementName() != null
+              && i < bitMaps.length
+              && isColumnAllNull(bitMaps[i], rowSize);
+      if (drop) {
+        continue;
+      }
+
+      keptSchemas.add(schemas.get(i));
+      if (keptCategories != null) {
+        keptCategories.add(category);
+      }
+      keptValues.add(values[i]);
+      keptBitMaps.add(i < bitMaps.length ? bitMaps[i] : null);
+    }
+
+    if (keptSchemas.size() == columnCount) {
+      return tablet;
+    }
+    if (keptSchemas.isEmpty()) {
+      return null;
+    }
+
+    Object[] newValues = keptValues.toArray();
+    BitMap[] newBitMaps = keptBitMaps.toArray(new BitMap[0]);
+    if (keptCategories != null) {
+      return new Tablet(
+          tablet.getDeviceId(),
+          keptSchemas,
+          keptCategories,
+          tablet.getTimestamps(),
+          newValues,
+          newBitMaps,
+          rowSize);
+    }
+    return new Tablet(
+        tablet.getDeviceId(), keptSchemas, tablet.getTimestamps(), newValues, 
newBitMaps, rowSize);
+  }
+
+  private static boolean isColumnAllNull(BitMap bitMap, int rowSize) {
+    if (bitMap == null || rowSize <= 0) {
+      return false;
+    }
+    // BitMap is sized to maxRowNumber; only [0, rowSize) are active rows.
+    return bitMap.isRangeAllMarked(0, rowSize);
+  }
+
   /* Used for table model insert only. */
   public static boolean isTabletContainsSingleDevice(Tablet tablet) {
     if (tablet.getRowSize() == 1) {
diff --git 
a/iotdb-client/session/src/test/java/org/apache/iotdb/session/util/SessionUtilsTest.java
 
b/iotdb-client/session/src/test/java/org/apache/iotdb/session/util/SessionUtilsTest.java
index ffdc5835dfc..b8524d4b311 100644
--- 
a/iotdb-client/session/src/test/java/org/apache/iotdb/session/util/SessionUtilsTest.java
+++ 
b/iotdb-client/session/src/test/java/org/apache/iotdb/session/util/SessionUtilsTest.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.session.util;
 import org.apache.iotdb.common.rpc.thrift.TEndPoint;
 import org.apache.iotdb.rpc.IoTDBConnectionException;
 
+import org.apache.tsfile.enums.ColumnCategory;
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.file.metadata.enums.CompressionType;
 import org.apache.tsfile.file.metadata.enums.TSEncoding;
@@ -224,6 +225,88 @@ public class SessionUtilsTest {
         () -> SessionUtils.getValueBuffer(typeList, valueList, measurements));
   }
 
+  @Test
+  public void testFilterNullColumns() {
+    List<IMeasurementSchema> schemas = new ArrayList<>();
+    schemas.add(new MeasurementSchema("s1", TSDataType.INT32));
+    schemas.add(new MeasurementSchema("s2", TSDataType.INT64));
+    schemas.add(new MeasurementSchema("s3", TSDataType.FLOAT));
+    schemas.add(new MeasurementSchema("s4", TSDataType.DOUBLE));
+    schemas.add(new MeasurementSchema("s5", TSDataType.BOOLEAN));
+
+    Tablet tablet = new Tablet("root.sg.d1", schemas, 1);
+    tablet.addTimestamp(0, 1000L);
+    tablet.addValue("s1", 0, 1);
+    tablet.addValue("s2", 0, null);
+    tablet.addValue("s3", 0, 1.5f);
+    tablet.addValue("s4", 0, null);
+    tablet.addValue("s5", 0, null);
+
+    Tablet filtered = SessionUtils.filterNullColumns(tablet);
+    Assert.assertNotNull(filtered);
+    Assert.assertNotSame(tablet, filtered);
+    Assert.assertEquals(2, filtered.getSchemas().size());
+    Assert.assertEquals("s1", 
filtered.getSchemas().get(0).getMeasurementName());
+    Assert.assertEquals("s3", 
filtered.getSchemas().get(1).getMeasurementName());
+    Assert.assertEquals(1, ((int[]) filtered.getValues()[0])[0]);
+    Assert.assertEquals(1.5f, ((float[]) filtered.getValues()[1])[0], 0.0001f);
+
+    // no BitMap -> same instance
+    long[] timestamps = new long[] {1L};
+    Object[] values = new Object[] {new int[] {1}};
+    List<IMeasurementSchema> singleSchema =
+        Collections.singletonList(new MeasurementSchema("s1", 
TSDataType.INT32));
+    Tablet noBitMapTablet = new Tablet("root.sg.d1", singleSchema, timestamps, 
values, null, 1);
+    Assert.assertSame(noBitMapTablet, 
SessionUtils.filterNullColumns(noBitMapTablet));
+
+    // all columns null -> null
+    Tablet allNull = new Tablet("root.sg.d1", schemas, 1);
+    allNull.addTimestamp(0, 2000L);
+    allNull.addValue("s1", 0, null);
+    allNull.addValue("s2", 0, null);
+    allNull.addValue("s3", 0, null);
+    allNull.addValue("s4", 0, null);
+    allNull.addValue("s5", 0, null);
+    Assert.assertNull(SessionUtils.filterNullColumns(allNull));
+
+    // table model: keep TAG when all FIELD columns are null
+    List<String> tableMeasurements = Arrays.asList("tag1", "s1", "s2");
+    List<TSDataType> tableDataTypes =
+        Arrays.asList(TSDataType.STRING, TSDataType.INT32, TSDataType.INT32);
+    List<ColumnCategory> columnCategories = new ArrayList<>();
+    columnCategories.add(ColumnCategory.TAG);
+    columnCategories.add(ColumnCategory.FIELD);
+    columnCategories.add(ColumnCategory.FIELD);
+    Tablet tableModelTablet =
+        new Tablet("table1", tableMeasurements, tableDataTypes, 
columnCategories, 1);
+    tableModelTablet.addTimestamp(0, 3000L);
+    tableModelTablet.addValue("tag1", 0, "d1");
+    tableModelTablet.addValue("s1", 0, null);
+    tableModelTablet.addValue("s2", 0, null);
+    Tablet tableModelFiltered = 
SessionUtils.filterNullColumns(tableModelTablet);
+    Assert.assertNotNull(tableModelFiltered);
+    Assert.assertNotSame(tableModelTablet, tableModelFiltered);
+    Assert.assertEquals(1, tableModelFiltered.getSchemas().size());
+    Assert.assertEquals("tag1", 
tableModelFiltered.getSchemas().get(0).getMeasurementName());
+    Assert.assertEquals(ColumnCategory.TAG, 
tableModelFiltered.getColumnTypes().get(0));
+  }
+
+  @Test
+  public void testFilterNullColumnsActiveRowsWhenBitmapSizedToMaxRowNumber() {
+    List<IMeasurementSchema> twoSchemas = new ArrayList<>();
+    twoSchemas.add(new MeasurementSchema("s1", TSDataType.INT32));
+    twoSchemas.add(new MeasurementSchema("s2", TSDataType.INT64));
+    Tablet partialRowTablet = new Tablet("root.sg.d1", twoSchemas, 10);
+    partialRowTablet.addTimestamp(0, 4000L);
+    partialRowTablet.addValue("s1", 0, 1);
+    partialRowTablet.addValue("s2", 0, null);
+    Tablet partialRowFiltered = 
SessionUtils.filterNullColumns(partialRowTablet);
+    Assert.assertNotNull(partialRowFiltered);
+    Assert.assertNotSame(partialRowTablet, partialRowFiltered);
+    Assert.assertEquals(1, partialRowFiltered.getSchemas().size());
+    Assert.assertEquals("s1", 
partialRowFiltered.getSchemas().get(0).getMeasurementName());
+  }
+
   @Test
   public void testParseSeedNodeUrls() {
     List<String> nodeUrls = Collections.singletonList("127.0.0.1:1234");

Reply via email to