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

JackieTien97 pushed a commit to branch codex/backport-pr-18149-dev-1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit 3c20c256a3933130c74c2472bd75432084a5dbdd
Author: JackieTien97 <[email protected]>
AuthorDate: Wed Jul 8 15:36:26 2026 +0800

    Remap non-positive REST row limit config to default and add regression tests
    
    A non-positive rest_query_default_row_size_limit used to mean "unlimited";
    the hard-cap handling clamped it down to 1, silently capping every REST
    response to a single row. Remap non-positive values to the built-in
    default (10000) so existing deployments are not degraded, while still
    guaranteeing a finite cap.
    
    Add regression coverage:
    - QueryRowLimitUtilsTest: an oversized caller-provided rowLimit is clamped
      to the configured hard limit.
    - FastLastHandlerTest: the fastLastQuery cache-hit path caps the number of
      materialized entries. The cache-hit loop is extracted into
      FastLastHandler.fillLastValueDataSet so it is unit-testable.
---
 .../protocol/rest/handler/QueryRowLimitUtils.java  |  12 ++-
 .../rest/v1/handler/QueryDataSetHandler.java       |   2 +-
 .../protocol/rest/v2/handler/FastLastHandler.java  |  59 ++++++++++-
 .../rest/v2/handler/QueryDataSetHandler.java       |   2 +-
 .../protocol/rest/v2/impl/RestApiServiceImpl.java  |  41 +-------
 .../rest/handler/QueryRowLimitUtilsTest.java       |  17 +++-
 .../rest/v1/handler/QueryDataSetHandlerTest.java   |   1 -
 .../rest/v2/handler/FastLastHandlerTest.java       | 108 +++++++++++++++++++++
 .../conf/iotdb-system.properties.template          |   1 +
 9 files changed, 191 insertions(+), 52 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/handler/QueryRowLimitUtils.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/handler/QueryRowLimitUtils.java
index ae3ceb7e61e..00f8a993ae8 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/handler/QueryRowLimitUtils.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/handler/QueryRowLimitUtils.java
@@ -26,7 +26,12 @@ import javax.ws.rs.core.Response;
 
 public final class QueryRowLimitUtils {
 
-  private static final int MIN_ROW_SIZE_LIMIT = 1;
+  /**
+   * Fallback used when {@code rest_query_default_row_size_limit} is missing 
or non-positive.
+   * Matches the built-in default in {@code IoTDBRestServiceConfig}; a 
non-positive configured value
+   * used to mean "unlimited" and is now treated as this cap instead of being 
clamped down to 1.
+   */
+  private static final int DEFAULT_ROW_SIZE_LIMIT = 10000;
 
   private QueryRowLimitUtils() {}
 
@@ -40,7 +45,7 @@ public final class QueryRowLimitUtils {
   }
 
   public static int normalizeRowSizeLimit(int rowSizeLimit) {
-    return Math.max(MIN_ROW_SIZE_LIMIT, rowSizeLimit);
+    return rowSizeLimit > 0 ? rowSizeLimit : DEFAULT_ROW_SIZE_LIMIT;
   }
 
   public static boolean exceedsLimit(
@@ -57,8 +62,7 @@ public final class QueryRowLimitUtils {
                 .code(TSStatusCode.QUERY_PROCESS_ERROR.getStatusCode())
                 .message(
                     String.format(
-                        "Dataset row size exceeded the given max row size 
(%d)",
-                        rowSizeLimit)))
+                        "Dataset row size exceeded the given max row size 
(%d)", rowSizeLimit)))
         .build();
   }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v1/handler/QueryDataSetHandler.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v1/handler/QueryDataSetHandler.java
index e774cdde00b..b87ace54df4 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v1/handler/QueryDataSetHandler.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v1/handler/QueryDataSetHandler.java
@@ -18,6 +18,7 @@
 package org.apache.iotdb.db.protocol.rest.v1.handler;
 
 import org.apache.iotdb.commons.exception.IoTDBException;
+import org.apache.iotdb.db.protocol.rest.handler.QueryRowLimitUtils;
 import org.apache.iotdb.db.protocol.rest.v1.model.ExecutionStatus;
 import org.apache.iotdb.db.queryengine.common.header.DatasetHeader;
 import org.apache.iotdb.db.queryengine.plan.execution.IQueryExecution;
@@ -28,7 +29,6 @@ import 
org.apache.iotdb.db.queryengine.plan.statement.metadata.ShowChildPathsSta
 import org.apache.iotdb.db.queryengine.plan.statement.metadata.ShowStatement;
 import 
org.apache.iotdb.db.queryengine.plan.statement.metadata.model.ShowModelsStatement;
 import org.apache.iotdb.db.queryengine.plan.statement.sys.AuthorStatement;
-import org.apache.iotdb.db.protocol.rest.handler.QueryRowLimitUtils;
 import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.tsfile.block.column.Column;
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/handler/FastLastHandler.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/handler/FastLastHandler.java
index 5d1c3d0109c..af1657a8b13 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/handler/FastLastHandler.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/handler/FastLastHandler.java
@@ -20,16 +20,25 @@
 package org.apache.iotdb.db.protocol.rest.v2.handler;
 
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.db.protocol.rest.handler.QueryRowLimitUtils;
 import org.apache.iotdb.db.protocol.rest.v2.model.ExecutionStatus;
 import org.apache.iotdb.db.protocol.rest.v2.model.PrefixPathList;
+import org.apache.iotdb.db.protocol.rest.v2.model.QueryDataSet;
 import org.apache.iotdb.db.protocol.session.IClientSession;
+import 
org.apache.iotdb.db.queryengine.plan.analyze.cache.schema.DeviceLastCache;
 import org.apache.iotdb.rpc.TSStatusCode;
 import org.apache.iotdb.service.rpc.thrift.TSLastDataQueryReq;
 
+import org.apache.tsfile.common.constant.TsFileConstant;
+import org.apache.tsfile.read.TimeValuePair;
+
 import javax.ws.rs.core.Response;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 
 public class FastLastHandler {
 
@@ -59,8 +68,7 @@ public class FastLastHandler {
         .build();
   }
 
-  public static void setupTargetDataSet(
-      org.apache.iotdb.db.protocol.rest.v2.model.QueryDataSet dataSet) {
+  public static void setupTargetDataSet(QueryDataSet dataSet) {
     dataSet.addExpressionsItem("Timeseries");
     dataSet.addExpressionsItem("Value");
     dataSet.addExpressionsItem("DataType");
@@ -70,4 +78,51 @@ public class FastLastHandler {
     dataSet.setValues(new ArrayList<>());
     dataSet.setTimestamps(new ArrayList<>());
   }
+
+  /**
+   * Builds the fastLastQuery response directly from cached last values. The 
number of materialized
+   * entries is capped by {@code actualRowSizeLimit}; once exceeded an 
"exceeded max row size"
+   * response is returned instead of materializing the whole cache into heap.
+   *
+   * @param resultMap last values keyed by device/measurement, as filled by 
the schema cache
+   * @param actualRowSizeLimit hard cap on the number of returned rows
+   */
+  public static Response fillLastValueDataSet(
+      Map<PartialPath, Map<String, TimeValuePair>> resultMap, int 
actualRowSizeLimit) {
+    QueryDataSet targetDataSet = new QueryDataSet();
+    setupTargetDataSet(targetDataSet);
+    List<Object> timeseries = new ArrayList<>();
+    List<Object> valueList = new ArrayList<>();
+    List<Object> dataTypeList = new ArrayList<>();
+    int fetched = 0;
+
+    for (final Map.Entry<PartialPath, Map<String, TimeValuePair>> 
device2MeasurementLastEntry :
+        resultMap.entrySet()) {
+      final String deviceWithSeparator =
+          device2MeasurementLastEntry.getKey() + TsFileConstant.PATH_SEPARATOR;
+      for (Map.Entry<String, TimeValuePair> measurementEntry :
+          device2MeasurementLastEntry.getValue().entrySet()) {
+        final TimeValuePair tvPair = measurementEntry.getValue();
+        if (tvPair == null
+            || tvPair == DeviceLastCache.EMPTY_TIME_VALUE_PAIR
+            || tvPair.getValue() == null) {
+          continue;
+        }
+        if (QueryRowLimitUtils.exceedsLimit(fetched, 1, actualRowSizeLimit)) {
+          return 
QueryRowLimitUtils.buildRowSizeLimitExceededResponse(actualRowSizeLimit);
+        }
+        valueList.add(tvPair.getValue().getStringValue());
+        dataTypeList.add(tvPair.getValue().getDataType().name());
+        targetDataSet.addTimestampsItem(tvPair.getTimestamp());
+        timeseries.add(deviceWithSeparator + measurementEntry.getKey());
+        fetched++;
+      }
+    }
+    if (!timeseries.isEmpty()) {
+      targetDataSet.addValuesItem(timeseries);
+      targetDataSet.addValuesItem(valueList);
+      targetDataSet.addValuesItem(dataTypeList);
+    }
+    return Response.ok().entity(targetDataSet).build();
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/handler/QueryDataSetHandler.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/handler/QueryDataSetHandler.java
index e423dc2539a..e77265abe1d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/handler/QueryDataSetHandler.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/handler/QueryDataSetHandler.java
@@ -18,6 +18,7 @@
 package org.apache.iotdb.db.protocol.rest.v2.handler;
 
 import org.apache.iotdb.commons.exception.IoTDBException;
+import org.apache.iotdb.db.protocol.rest.handler.QueryRowLimitUtils;
 import org.apache.iotdb.db.protocol.rest.model.ExecutionStatus;
 import org.apache.iotdb.db.queryengine.common.header.DatasetHeader;
 import org.apache.iotdb.db.queryengine.plan.execution.IQueryExecution;
@@ -28,7 +29,6 @@ import 
org.apache.iotdb.db.queryengine.plan.statement.metadata.ShowChildPathsSta
 import org.apache.iotdb.db.queryengine.plan.statement.metadata.ShowStatement;
 import 
org.apache.iotdb.db.queryengine.plan.statement.metadata.model.ShowModelsStatement;
 import org.apache.iotdb.db.queryengine.plan.statement.sys.AuthorStatement;
-import org.apache.iotdb.db.protocol.rest.handler.QueryRowLimitUtils;
 import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.tsfile.block.column.Column;
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/impl/RestApiServiceImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/impl/RestApiServiceImpl.java
index cea8d4ea409..80e86c1a998 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/impl/RestApiServiceImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/protocol/rest/v2/impl/RestApiServiceImpl.java
@@ -37,7 +37,6 @@ import 
org.apache.iotdb.db.protocol.rest.v2.handler.StatementConstructionHandler
 import org.apache.iotdb.db.protocol.rest.v2.model.InsertRecordsRequest;
 import org.apache.iotdb.db.protocol.rest.v2.model.InsertTabletRequest;
 import org.apache.iotdb.db.protocol.rest.v2.model.PrefixPathList;
-import org.apache.iotdb.db.protocol.rest.v2.model.QueryDataSet;
 import org.apache.iotdb.db.protocol.rest.v2.model.SQL;
 import org.apache.iotdb.db.protocol.session.IClientSession;
 import org.apache.iotdb.db.protocol.session.SessionManager;
@@ -47,7 +46,6 @@ import org.apache.iotdb.db.queryengine.plan.Coordinator;
 import org.apache.iotdb.db.queryengine.plan.analyze.ClusterPartitionFetcher;
 import org.apache.iotdb.db.queryengine.plan.analyze.IPartitionFetcher;
 import 
org.apache.iotdb.db.queryengine.plan.analyze.cache.schema.DataNodeSchemaCache;
-import 
org.apache.iotdb.db.queryengine.plan.analyze.cache.schema.DeviceLastCache;
 import 
org.apache.iotdb.db.queryengine.plan.analyze.schema.ClusterSchemaFetcher;
 import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaFetcher;
 import org.apache.iotdb.db.queryengine.plan.execution.ExecutionResult;
@@ -64,14 +62,12 @@ import org.apache.iotdb.db.utils.SetThreadName;
 import org.apache.iotdb.rpc.TSStatusCode;
 import org.apache.iotdb.service.rpc.thrift.TSLastDataQueryReq;
 
-import org.apache.tsfile.common.constant.TsFileConstant;
 import org.apache.tsfile.read.TimeValuePair;
 
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.SecurityContext;
 
 import java.time.ZoneId;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -169,40 +165,9 @@ public class RestApiServiceImpl extends RestApiService {
         }
       }
 
-      // Cache hit: build response directly
-      QueryDataSet targetDataSet = new QueryDataSet();
-
-      FastLastHandler.setupTargetDataSet(targetDataSet);
-      List<Object> timeseries = new ArrayList<>();
-      List<Object> valueList = new ArrayList<>();
-      List<Object> dataTypeList = new ArrayList<>();
-      int fetched = 0;
-
-      for (final Map.Entry<PartialPath, Map<String, TimeValuePair>> 
device2MeasurementLastEntry :
-          resultMap.entrySet()) {
-        final String deviceWithSeparator =
-            device2MeasurementLastEntry.getKey() + 
TsFileConstant.PATH_SEPARATOR;
-        for (Map.Entry<String, TimeValuePair> measurementEntry :
-            device2MeasurementLastEntry.getValue().entrySet()) {
-          final TimeValuePair tvPair = measurementEntry.getValue();
-          if (tvPair != DeviceLastCache.EMPTY_TIME_VALUE_PAIR) {
-            if (QueryRowLimitUtils.exceedsLimit(fetched, 1, 
defaultQueryRowLimit)) {
-              return 
QueryRowLimitUtils.buildRowSizeLimitExceededResponse(defaultQueryRowLimit);
-            }
-            valueList.add(tvPair.getValue().getStringValue());
-            dataTypeList.add(tvPair.getValue().getDataType().name());
-            targetDataSet.addTimestampsItem(tvPair.getTimestamp());
-            timeseries.add(deviceWithSeparator + measurementEntry.getKey());
-            fetched++;
-          }
-        }
-      }
-      if (!timeseries.isEmpty()) {
-        targetDataSet.addValuesItem(timeseries);
-        targetDataSet.addValuesItem(valueList);
-        targetDataSet.addValuesItem(dataTypeList);
-      }
-      return Response.ok().entity(targetDataSet).build();
+      // Cache hit: build response directly (capped by defaultQueryRowLimit).
+      finish = true;
+      return FastLastHandler.fillLastValueDataSet(resultMap, 
defaultQueryRowLimit);
 
     } catch (Exception e) {
       finish = true;
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/handler/QueryRowLimitUtilsTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/handler/QueryRowLimitUtilsTest.java
index d1d9f2a42a0..3f21e8ea167 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/handler/QueryRowLimitUtilsTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/handler/QueryRowLimitUtilsTest.java
@@ -32,13 +32,18 @@ public class QueryRowLimitUtilsTest {
     assertEquals(10, QueryRowLimitUtils.resolveActualRowSizeLimit(null, 10));
     assertEquals(10, QueryRowLimitUtils.resolveActualRowSizeLimit(100, 10));
     assertEquals(5, QueryRowLimitUtils.resolveActualRowSizeLimit(5, 10));
+    // A caller-provided MAX_VALUE must be clamped down to the configured hard 
limit.
+    assertEquals(10, 
QueryRowLimitUtils.resolveActualRowSizeLimit(Integer.MAX_VALUE, 10));
   }
 
   @Test
-  public void resolveActualRowSizeLimitShouldKeepHardLimitPositive() {
-    assertEquals(1, QueryRowLimitUtils.resolveActualRowSizeLimit(null, 0));
-    assertEquals(1, QueryRowLimitUtils.resolveActualRowSizeLimit(100, 0));
-    assertEquals(1, QueryRowLimitUtils.resolveActualRowSizeLimit(null, -1));
+  public void 
resolveActualRowSizeLimitShouldFallBackToDefaultForNonPositiveConfig() {
+    // A non-positive rest_query_default_row_size_limit used to mean 
"unlimited"; it now falls back
+    // to the built-in default (10000) instead of being clamped down to a 
single row.
+    assertEquals(10000, QueryRowLimitUtils.resolveActualRowSizeLimit(null, 0));
+    // A user request below the cap is still honored: min(100, default 10000) 
== 100.
+    assertEquals(100, QueryRowLimitUtils.resolveActualRowSizeLimit(100, 0));
+    assertEquals(10000, QueryRowLimitUtils.resolveActualRowSizeLimit(null, 
-1));
   }
 
   @Test
@@ -47,6 +52,8 @@ public class QueryRowLimitUtilsTest {
     assertTrue(QueryRowLimitUtils.exceedsLimit(2, 1, 2));
     assertTrue(QueryRowLimitUtils.exceedsLimit(0, 2, 1));
     assertFalse(QueryRowLimitUtils.exceedsLimit(0, 0, 1));
-    assertTrue(QueryRowLimitUtils.exceedsLimit(0, 2, 0));
+    // A non-positive limit falls back to the default (10000), so small 
batches do not exceed it.
+    assertFalse(QueryRowLimitUtils.exceedsLimit(0, 2, 0));
+    assertTrue(QueryRowLimitUtils.exceedsLimit(0, 10001, 0));
   }
 }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/v1/handler/QueryDataSetHandlerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/v1/handler/QueryDataSetHandlerTest.java
index cc3f1a12bed..9f5d3a097ea 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/v1/handler/QueryDataSetHandlerTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/v1/handler/QueryDataSetHandlerTest.java
@@ -196,6 +196,5 @@ public class QueryDataSetHandlerTest {
     public String getClientHostname() {
       return null;
     }
-
   }
 }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/v2/handler/FastLastHandlerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/v2/handler/FastLastHandlerTest.java
new file mode 100644
index 00000000000..786db7492d1
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/protocol/rest/v2/handler/FastLastHandlerTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.iotdb.db.protocol.rest.v2.handler;
+
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.db.protocol.rest.model.ExecutionStatus;
+import org.apache.iotdb.db.protocol.rest.v2.model.QueryDataSet;
+import 
org.apache.iotdb.db.queryengine.plan.analyze.cache.schema.DeviceLastCache;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.TimeValuePair;
+import org.apache.tsfile.utils.TsPrimitiveType;
+import org.junit.Test;
+
+import javax.ws.rs.core.Response;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class FastLastHandlerTest {
+
+  private static final String DEVICE = "root.sg25.d1";
+
+  @Test
+  public void fillLastValueDataSetShouldReturnAllEntriesWhenUnderLimit() 
throws Exception {
+    Map<PartialPath, Map<String, TimeValuePair>> resultMap = 
newResultMap("s1", "s2", "s3");
+
+    Response response = FastLastHandler.fillLastValueDataSet(resultMap, 5);
+
+    assertTrue(response.getEntity() instanceof QueryDataSet);
+    QueryDataSet dataSet = (QueryDataSet) response.getEntity();
+    assertEquals(3, dataSet.getTimestamps().size());
+    // [timeseries, valueList, dataTypeList], each holding one row per 
measurement
+    assertEquals(3, dataSet.getValues().size());
+    assertEquals(3, dataSet.getValues().get(0).size());
+  }
+
+  @Test
+  public void fillLastValueDataSetShouldCapAtLimitAndReportError() throws 
Exception {
+    // Regression for the fastLastQuery cache-hit path: a warm cache with more 
entries than the
+    // configured limit must NOT materialize the whole result into heap.
+    Map<PartialPath, Map<String, TimeValuePair>> resultMap =
+        newResultMap("s1", "s2", "s3", "s4", "s5");
+
+    Response response = FastLastHandler.fillLastValueDataSet(resultMap, 2);
+
+    assertTrue(response.getEntity() instanceof ExecutionStatus);
+    ExecutionStatus status = (ExecutionStatus) response.getEntity();
+    assertEquals(TSStatusCode.QUERY_PROCESS_ERROR.getStatusCode(), 
status.getCode().intValue());
+  }
+
+  @Test
+  public void fillLastValueDataSetShouldSkipPlaceholderAndNullEntries() throws 
Exception {
+    Map<PartialPath, Map<String, TimeValuePair>> resultMap =
+        newResultMap("s1", "placeholder", "nullValue", "s4");
+
+    Response response = FastLastHandler.fillLastValueDataSet(resultMap, 10);
+
+    assertTrue(response.getEntity() instanceof QueryDataSet);
+    QueryDataSet dataSet = (QueryDataSet) response.getEntity();
+    // Only the two real entries (s1, s4) are materialized; placeholder/null 
entries are skipped.
+    assertEquals(2, dataSet.getTimestamps().size());
+  }
+
+  /** Builds a single-device resultMap; the special names insert sentinel 
entries. */
+  private static Map<PartialPath, Map<String, TimeValuePair>> newResultMap(
+      String... measurementNames) throws Exception {
+    Map<String, TimeValuePair> measurements = new HashMap<>();
+    long timestamp = 100L;
+    for (String name : measurementNames) {
+      measurements.put(name, newEntry(name, timestamp++));
+    }
+    Map<PartialPath, Map<String, TimeValuePair>> resultMap = new HashMap<>();
+    resultMap.put(new PartialPath(DEVICE), measurements);
+    return resultMap;
+  }
+
+  private static TimeValuePair newEntry(String name, long timestamp) {
+    if ("placeholder".equals(name)) {
+      return DeviceLastCache.EMPTY_TIME_VALUE_PAIR;
+    }
+    if ("nullValue".equals(name)) {
+      return new TimeValuePair(timestamp, null);
+    }
+    return new TimeValuePair(timestamp, 
TsPrimitiveType.getByType(TSDataType.INT64, timestamp));
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
 
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
index f3d5f36c2ec..bda26c3cb93 100644
--- 
a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
+++ 
b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
@@ -565,6 +565,7 @@ enable_swagger=false
 
 # The maximum row limit for REST and Grafana query responses.
 # The request rowLimit/row_limit value cannot exceed this limit.
+# A non-positive value is invalid and falls back to the default (10000); it no 
longer means unlimited.
 # effectiveMode: restart
 # Datatype: int
 rest_query_default_row_size_limit=10000

Reply via email to