From 2990200efc6d93cb1dea55ea98614f466e9e074a Mon Sep 17 00:00:00 2001 From: 761417898 <761417898@qq.com> Date: Tue, 15 Sep 2026 15:28:27 +0800 Subject: [PATCH 1/5] Optimize Python session result decoding --- iotdb-client/client-py/iotdb/utils/Field.py | 69 +++++------ .../client-py/iotdb/utils/SessionDataSet.py | 29 ++--- .../iotdb/utils/iotdb_rpc_dataset.py | 109 +++++++++++++++--- .../client-py/iotdb/utils/rpc_utils.py | 12 +- 4 files changed, 137 insertions(+), 82 deletions(-) diff --git a/iotdb-client/client-py/iotdb/utils/Field.py b/iotdb-client/client-py/iotdb/utils/Field.py index d9a0ee77776ec..4469115373175 100644 --- a/iotdb-client/client-py/iotdb/utils/Field.py +++ b/iotdb-client/client-py/iotdb/utils/Field.py @@ -16,12 +16,18 @@ # under the License. # -# for package -from iotdb.utils.IoTDBConstants import TSDataType -from iotdb.tsfile.utils.date_utils import parse_int_to_date -from iotdb.utils.rpc_utils import convert_to_timestamp, isoformat import numpy as np -import pandas as pd +from iotdb.tsfile.utils.date_utils import parse_int_to_date +from iotdb.utils.IoTDBConstants import TSDataType + + +def _is_missing(value): + if value is None: + return True + value_type = type(value) + return value_type.__name__ == "NAType" and value_type.__module__.startswith( + "pandas." + ) class Field(object): @@ -70,7 +76,7 @@ def get_data_type(self): return self.__data_type def is_null(self): - return self.__data_type is None or self.value is None or self.value is pd.NA + return self.__data_type is None or _is_missing(self.value) def set_bool_value(self, value: bool): self.value = value @@ -78,11 +84,7 @@ def set_bool_value(self, value: bool): def get_bool_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if ( - self.__data_type != TSDataType.BOOLEAN - or self.value is None - or self.value is pd.NA - ): + if self.__data_type != TSDataType.BOOLEAN or _is_missing(self.value): return None return self.value @@ -95,8 +97,7 @@ def get_int_value(self): if ( self.__data_type != TSDataType.INT32 and self.__data_type != TSDataType.DATE - or self.value is None - or self.value is pd.NA + or _is_missing(self.value) ): return None return np.int32(self.value) @@ -110,8 +111,7 @@ def get_long_value(self): if ( self.__data_type != TSDataType.INT64 and self.__data_type != TSDataType.TIMESTAMP - or self.value is None - or self.value is pd.NA + or _is_missing(self.value) ): return None return np.int64(self.value) @@ -122,11 +122,7 @@ def set_float_value(self, value: float): def get_float_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if ( - self.__data_type != TSDataType.FLOAT - or self.value is None - or self.value is pd.NA - ): + if self.__data_type != TSDataType.FLOAT or _is_missing(self.value): return None return np.float32(self.value) @@ -136,11 +132,7 @@ def set_double_value(self, value: float): def get_double_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if ( - self.__data_type != TSDataType.DOUBLE - or self.value is None - or self.value is pd.NA - ): + if self.__data_type != TSDataType.DOUBLE or _is_missing(self.value): return None return np.float64(self.value) @@ -154,8 +146,7 @@ def get_binary_value(self): self.__data_type != TSDataType.TEXT and self.__data_type != TSDataType.STRING and self.__data_type != TSDataType.BLOB - or self.value is None - or self.value is pd.NA + or _is_missing(self.value) ): return None return self.value @@ -163,27 +154,21 @@ def get_binary_value(self): def get_timestamp_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if ( - self.__data_type != TSDataType.TIMESTAMP - or self.value is None - or self.value is pd.NA - ): + if self.__data_type != TSDataType.TIMESTAMP or _is_missing(self.value): return None + from iotdb.utils.rpc_utils import convert_to_timestamp + return convert_to_timestamp(self.value, self.__precision, self.__timezone) def get_date_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if ( - self.__data_type != TSDataType.DATE - or self.value is None - or self.value is pd.NA - ): + if self.__data_type != TSDataType.DATE or _is_missing(self.value): return None return parse_int_to_date(self.value) def get_string_value(self): - if self.__data_type is None or self.value is None or self.value is pd.NA: + if self.__data_type is None or _is_missing(self.value): return "None" # TEXT, STRING if self.__data_type == 5 or self.__data_type == 11: @@ -193,6 +178,8 @@ def get_string_value(self): return str(hex(int.from_bytes(self.value, byteorder="big"))) # TIMESTAMP elif self.__data_type == 8: + from iotdb.utils.rpc_utils import convert_to_timestamp, isoformat + return isoformat( convert_to_timestamp(self.value, self.__precision, self.__timezone), self.__precision, @@ -208,7 +195,7 @@ def get_object_value(self, data_type): """ :param data_type: TSDataType """ - if self.__data_type is None or self.value is None or self.value is pd.NA: + if self.__data_type is None or _is_missing(self.value): return None if data_type == 0: return bool(self.value) @@ -221,6 +208,8 @@ def get_object_value(self, data_type): elif data_type == 4: return np.float64(self.value) elif data_type == 8: + from iotdb.utils.rpc_utils import convert_to_timestamp + return convert_to_timestamp(self.value, self.__precision, self.__timezone) elif data_type == 9: return parse_int_to_date(self.value) @@ -237,7 +226,7 @@ def get_field(value, data_type): :param value: field value corresponding to the data type :param data_type: TSDataType """ - if value is None or value is pd.NA: + if _is_missing(value): return None field = Field(data_type, value) return field diff --git a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py index 9c5b3ec06ddc5..f3730c64933c7 100644 --- a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py +++ b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py @@ -16,16 +16,17 @@ # under the License. # import logging -from typing import Optional +from typing import TYPE_CHECKING, Optional from iotdb.utils.Field import Field +from iotdb.utils.iotdb_rpc_dataset import IoTDBRpcDataSet # for package from iotdb.utils.IoTDBConstants import TSDataType -from iotdb.utils.iotdb_rpc_dataset import IoTDBRpcDataSet from iotdb.utils.RowRecord import RowRecord -import pandas as pd +if TYPE_CHECKING: + import pandas as pd logger = logging.getLogger("IoTDB") @@ -109,14 +110,14 @@ def has_next(self): return self.iotdb_rpc_data_set.next() def next(self): - if not self.iotdb_rpc_data_set.has_cached_data_frame: - if not self.has_next(): - return None + if not self.has_next(): + return None return self.construct_row_record_from_data_frame() def construct_row_record_from_data_frame(self): - df = self.iotdb_rpc_data_set.data_frame - row = df.iloc[self.row_index].to_list() + row = self.iotdb_rpc_data_set._pop_row() + if row is None: + return None if self.iotdb_rpc_data_set.ignore_timestamp: for field, value in zip(self.__field_list, row): field.value = value @@ -133,12 +134,6 @@ def construct_row_record_from_data_frame(self): row[0], self.__field_list, ) - self.row_index += 1 - if self.row_index == len(df): - self.row_index = 0 - self.iotdb_rpc_data_set.has_cached_data_frame = False - self.iotdb_rpc_data_set.data_frame = None - return row_record def close_operation_handle(self): @@ -153,7 +148,7 @@ def has_next_df(self) -> bool: rpc_ds = self.iotdb_rpc_data_set return rpc_ds._has_buffered_data() or rpc_ds._has_next_result_set() - def next_df(self) -> Optional[pd.DataFrame]: + def next_df(self) -> Optional["pd.DataFrame"]: """ Get the next DataFrame from the result set. Each returned DataFrame contains exactly fetch_size rows, @@ -162,11 +157,11 @@ def next_df(self) -> Optional[pd.DataFrame]: """ return self.iotdb_rpc_data_set.next_dataframe() - def todf(self) -> pd.DataFrame: + def todf(self) -> "pd.DataFrame": return result_set_to_pandas(self) -def result_set_to_pandas(result_set: SessionDataSet) -> pd.DataFrame: +def result_set_to_pandas(result_set: SessionDataSet) -> "pd.DataFrame": """ Transforms a SessionDataSet from IoTDB to a Pandas Data Frame Each Field from IoTDB is a column in Pandas diff --git a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py index 0edc76f68fd6d..cf43c8bd6f823 100644 --- a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py +++ b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py @@ -18,18 +18,19 @@ # for package import logging -from typing import Optional +from collections import deque +from typing import TYPE_CHECKING, Optional import numpy as np -import pandas as pd -from thrift.transport import TTransport - -from iotdb.thrift.rpc.IClientRPCService import TSFetchResultsReq, TSCloseOperationReq -from iotdb.tsfile.utils.date_utils import parse_int_to_date +from iotdb.thrift.rpc.IClientRPCService import TSCloseOperationReq, TSFetchResultsReq from iotdb.tsfile.utils.tsblock_serde import deserialize from iotdb.utils.exception import IoTDBConnectionException from iotdb.utils.IoTDBConstants import TSDataType -from iotdb.utils.rpc_utils import verify_success, convert_to_timestamp +from iotdb.utils.rpc_utils import verify_success +from thrift.transport import TTransport + +if TYPE_CHECKING: + import pandas as pd logger = logging.getLogger("IoTDB") TIMESTAMP_STR = "Time" @@ -122,11 +123,13 @@ def __init__( self.__zone_id = zone_id self.__time_precision = time_precision self.__df_buffer = None # Buffer for streaming DataFrames + self.__row_buffer = deque() def close(self): if self.__is_closed: return self.__df_buffer = None # Clean up streaming DataFrame buffer + self.__row_buffer.clear() if self.__client is not None: try: status = self.__client.closeOperation( @@ -148,18 +151,78 @@ def close(self): self.__client = None def next(self): - if not self.has_cached_data_frame: - self.construct_one_data_frame() - if self.has_cached_data_frame: - return True - if self.__empty_resultSet: - return False - if self.__more_data and self.fetch_results(): - self.construct_one_data_frame() - return True - return False + return bool(self.__row_buffer) or self._fill_row_buffer() + + def _pop_row(self): + if not self.next(): + return None + return self.__row_buffer.popleft() + + def _pop_rows(self, max_rows=None): + rows = [] + while max_rows is None or len(rows) < max_rows: + if not self.__row_buffer and not self._fill_row_buffer(): + break + remaining = ( + len(self.__row_buffer) + if max_rows is None + else min(len(self.__row_buffer), max_rows - len(rows)) + ) + rows.extend(self.__row_buffer.popleft() for _ in range(remaining)) + return rows + + def _fill_row_buffer(self): + while not self.__row_buffer: + while self.__query_result is not None and self.__query_result_index < len( + self.__query_result + ): + block = self.__query_result[self.__query_result_index] + self.__query_result[self.__query_result_index] = None + self.__query_result_index += 1 + self.__row_buffer.extend(self._deserialize_rows(block)) + if self.__row_buffer: + return True + if self.__empty_resultSet or not self.__more_data: + return False + if not self.fetch_results(): + return False + return True + + def _deserialize_rows(self, serialized_block): + time_array, column_arrays, null_indicators, row_count = deserialize( + memoryview(serialized_block) + ) + columns = [] + if not self.ignore_timestamp: + columns.append(time_array.tolist()) + for location in self.__column_index_2_tsblock_column_index_list: + if location < 0: + continue + columns.append( + self._expand_column( + column_arrays[location], + null_indicators[location], + row_count, + self.__data_type_for_tsblock_column[location], + ) + ) + return list(zip(*columns)) if columns else [tuple() for _ in range(row_count)] + + @staticmethod + def _expand_column(values, nulls, row_count, data_type): + source = values.tolist() if hasattr(values, "tolist") else list(values) + if nulls is None: + return source + if data_type == TSDataType.BOOLEAN and len(source) == row_count: + return [ + None if nulls[index] else source[index] for index in range(row_count) + ] + source_values = iter(source) + return [None if is_null else next(source_values) for is_null in nulls] def construct_one_data_frame(self): + import pandas as pd + if self.has_cached_data_frame or self.__query_result is None: return result = {} @@ -233,7 +296,7 @@ def construct_one_data_frame(self): self.has_cached_data_frame = True def has_cached_result(self): - return self.has_cached_data_frame + return bool(self.__row_buffer) or self.has_cached_data_frame def _has_next_result_set(self): if (self.__query_result is not None) and ( @@ -253,12 +316,14 @@ def _has_buffered_data(self) -> bool: """ return self.__df_buffer is not None and len(self.__df_buffer) > 0 - def next_dataframe(self) -> Optional[pd.DataFrame]: + def next_dataframe(self) -> Optional["pd.DataFrame"]: """ Get the next DataFrame from the result set with exactly fetch_size rows. The last DataFrame may have fewer rows. :return: the next DataFrame with fetch_size rows, or None if no more data """ + import pandas as pd + # Accumulate data until we have at least fetch_size rows or no more data while True: buffer_len = 0 if self.__df_buffer is None else len(self.__df_buffer) @@ -308,6 +373,10 @@ def result_set_to_pandas(self): return self._build_dataframe(result) def _process_buffer(self): + import pandas as pd + from iotdb.tsfile.utils.date_utils import parse_int_to_date + from iotdb.utils.rpc_utils import convert_to_timestamp + result = {} for i in range(len(self.__column_index_2_tsblock_column_index_list)): result[i] = [] @@ -407,6 +476,8 @@ def _process_buffer(self): return result def _build_dataframe(self, result): + import pandas as pd + for k, v in result.items(): if v is None or len(v) < 1 or v[0] is None: result[k] = [] diff --git a/iotdb-client/client-py/iotdb/utils/rpc_utils.py b/iotdb-client/client-py/iotdb/utils/rpc_utils.py index 0912fd14d95de..5eb95d95b954a 100644 --- a/iotdb-client/client-py/iotdb/utils/rpc_utils.py +++ b/iotdb-client/client-py/iotdb/utils/rpc_utils.py @@ -17,10 +17,6 @@ # import logging -import pandas as pd -from pandas._libs import OutOfBoundsDatetime -from tzlocal import get_localzone_name - from iotdb.thrift.common.ttypes import TSStatus from iotdb.utils.exception import RedirectException, StatementExecutionException @@ -78,6 +74,10 @@ def verify_success_with_redirection_for_multi_devices(status: TSStatus, devices: def convert_to_timestamp(time: int, precision: str, timezone: str): + import pandas as pd + from pandas._libs import OutOfBoundsDatetime + from tzlocal import get_localzone_name + try: ts = pd.Timestamp(time, unit=precision, tz=timezone) except OutOfBoundsDatetime: @@ -116,7 +116,7 @@ def convert_to_timestamp(time: int, precision: str, timezone: str): } -def isoformat(ts: pd.Timestamp, unit: str): +def isoformat(ts, unit: str): if unit not in unit_map: raise ValueError(f"Unsupported unit: {unit}") try: @@ -134,7 +134,7 @@ def isoformat(ts: pd.Timestamp, unit: str): return _isoformat_from_components(ts, unit) -def _isoformat_from_components(ts: pd.Timestamp, unit: str) -> str: +def _isoformat_from_components(ts, unit: str) -> str: base = ( f"{ts.year:04d}-{ts.month:02d}-{ts.day:02d}" f"T{ts.hour:02d}:{ts.minute:02d}:{ts.second:02d}" From d9c053b6050fd1aa99aaab771546738386751fb6 Mon Sep 17 00:00:00 2001 From: 761417898 <761417898@qq.com> Date: Tue, 15 Sep 2026 16:10:48 +0800 Subject: [PATCH 2/5] Clarify Python result decoding logic --- iotdb-client/client-py/iotdb/utils/Field.py | 1 + iotdb-client/client-py/iotdb/utils/SessionDataSet.py | 1 + iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py | 2 ++ 3 files changed, 4 insertions(+) diff --git a/iotdb-client/client-py/iotdb/utils/Field.py b/iotdb-client/client-py/iotdb/utils/Field.py index 4469115373175..8cf94a21f49d7 100644 --- a/iotdb-client/client-py/iotdb/utils/Field.py +++ b/iotdb-client/client-py/iotdb/utils/Field.py @@ -24,6 +24,7 @@ def _is_missing(value): if value is None: return True + # Recognize pandas.NA without importing pandas on non-DataFrame paths. value_type = type(value) return value_type.__name__ == "NAType" and value_type.__module__.startswith( "pandas." diff --git a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py index f3730c64933c7..bfc9a513f2e95 100644 --- a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py +++ b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py @@ -115,6 +115,7 @@ def next(self): return self.construct_row_record_from_data_frame() def construct_row_record_from_data_frame(self): + # Preserve the legacy RowRecord API while consuming the tuple row buffer. row = self.iotdb_rpc_data_set._pop_row() if row is None: return None diff --git a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py index cf43c8bd6f823..89fe2fafa2688 100644 --- a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py +++ b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py @@ -177,6 +177,7 @@ def _fill_row_buffer(self): self.__query_result ): block = self.__query_result[self.__query_result_index] + # Mark the serialized block consumed so it can be released after decoding. self.__query_result[self.__query_result_index] = None self.__query_result_index += 1 self.__row_buffer.extend(self._deserialize_rows(block)) @@ -213,6 +214,7 @@ def _expand_column(values, nulls, row_count, data_type): source = values.tolist() if hasattr(values, "tolist") else list(values) if nulls is None: return source + # Boolean blocks retain positional values; other nullable blocks omit nulls. if data_type == TSDataType.BOOLEAN and len(source) == row_count: return [ None if nulls[index] else source[index] for index in range(row_count) From d8ec8c1d2c26b7fe22eb7c340e4d3fbd90bd50a3 Mon Sep 17 00:00:00 2001 From: 761417898 <761417898@qq.com> Date: Tue, 15 Sep 2026 16:13:21 +0800 Subject: [PATCH 3/5] Revert "Clarify Python result decoding logic" This reverts commit d9c053b6050fd1aa99aaab771546738386751fb6. --- iotdb-client/client-py/iotdb/utils/Field.py | 1 - iotdb-client/client-py/iotdb/utils/SessionDataSet.py | 1 - iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py | 2 -- 3 files changed, 4 deletions(-) diff --git a/iotdb-client/client-py/iotdb/utils/Field.py b/iotdb-client/client-py/iotdb/utils/Field.py index 8cf94a21f49d7..4469115373175 100644 --- a/iotdb-client/client-py/iotdb/utils/Field.py +++ b/iotdb-client/client-py/iotdb/utils/Field.py @@ -24,7 +24,6 @@ def _is_missing(value): if value is None: return True - # Recognize pandas.NA without importing pandas on non-DataFrame paths. value_type = type(value) return value_type.__name__ == "NAType" and value_type.__module__.startswith( "pandas." diff --git a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py index bfc9a513f2e95..f3730c64933c7 100644 --- a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py +++ b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py @@ -115,7 +115,6 @@ def next(self): return self.construct_row_record_from_data_frame() def construct_row_record_from_data_frame(self): - # Preserve the legacy RowRecord API while consuming the tuple row buffer. row = self.iotdb_rpc_data_set._pop_row() if row is None: return None diff --git a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py index 89fe2fafa2688..cf43c8bd6f823 100644 --- a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py +++ b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py @@ -177,7 +177,6 @@ def _fill_row_buffer(self): self.__query_result ): block = self.__query_result[self.__query_result_index] - # Mark the serialized block consumed so it can be released after decoding. self.__query_result[self.__query_result_index] = None self.__query_result_index += 1 self.__row_buffer.extend(self._deserialize_rows(block)) @@ -214,7 +213,6 @@ def _expand_column(values, nulls, row_count, data_type): source = values.tolist() if hasattr(values, "tolist") else list(values) if nulls is None: return source - # Boolean blocks retain positional values; other nullable blocks omit nulls. if data_type == TSDataType.BOOLEAN and len(source) == row_count: return [ None if nulls[index] else source[index] for index in range(row_count) From 076547c28bee07e6d384969ee532426fb821c1a7 Mon Sep 17 00:00:00 2001 From: 761417898 <761417898@qq.com> Date: Tue, 15 Sep 2026 16:20:13 +0800 Subject: [PATCH 4/5] Keep pandas imports in result decoding --- iotdb-client/client-py/iotdb/utils/Field.py | 69 +++++++++++-------- .../client-py/iotdb/utils/SessionDataSet.py | 12 ++-- .../iotdb/utils/iotdb_rpc_dataset.py | 21 ++---- .../client-py/iotdb/utils/rpc_utils.py | 12 ++-- 4 files changed, 56 insertions(+), 58 deletions(-) diff --git a/iotdb-client/client-py/iotdb/utils/Field.py b/iotdb-client/client-py/iotdb/utils/Field.py index 4469115373175..d9a0ee77776ec 100644 --- a/iotdb-client/client-py/iotdb/utils/Field.py +++ b/iotdb-client/client-py/iotdb/utils/Field.py @@ -16,18 +16,12 @@ # under the License. # -import numpy as np -from iotdb.tsfile.utils.date_utils import parse_int_to_date +# for package from iotdb.utils.IoTDBConstants import TSDataType - - -def _is_missing(value): - if value is None: - return True - value_type = type(value) - return value_type.__name__ == "NAType" and value_type.__module__.startswith( - "pandas." - ) +from iotdb.tsfile.utils.date_utils import parse_int_to_date +from iotdb.utils.rpc_utils import convert_to_timestamp, isoformat +import numpy as np +import pandas as pd class Field(object): @@ -76,7 +70,7 @@ def get_data_type(self): return self.__data_type def is_null(self): - return self.__data_type is None or _is_missing(self.value) + return self.__data_type is None or self.value is None or self.value is pd.NA def set_bool_value(self, value: bool): self.value = value @@ -84,7 +78,11 @@ def set_bool_value(self, value: bool): def get_bool_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if self.__data_type != TSDataType.BOOLEAN or _is_missing(self.value): + if ( + self.__data_type != TSDataType.BOOLEAN + or self.value is None + or self.value is pd.NA + ): return None return self.value @@ -97,7 +95,8 @@ def get_int_value(self): if ( self.__data_type != TSDataType.INT32 and self.__data_type != TSDataType.DATE - or _is_missing(self.value) + or self.value is None + or self.value is pd.NA ): return None return np.int32(self.value) @@ -111,7 +110,8 @@ def get_long_value(self): if ( self.__data_type != TSDataType.INT64 and self.__data_type != TSDataType.TIMESTAMP - or _is_missing(self.value) + or self.value is None + or self.value is pd.NA ): return None return np.int64(self.value) @@ -122,7 +122,11 @@ def set_float_value(self, value: float): def get_float_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if self.__data_type != TSDataType.FLOAT or _is_missing(self.value): + if ( + self.__data_type != TSDataType.FLOAT + or self.value is None + or self.value is pd.NA + ): return None return np.float32(self.value) @@ -132,7 +136,11 @@ def set_double_value(self, value: float): def get_double_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if self.__data_type != TSDataType.DOUBLE or _is_missing(self.value): + if ( + self.__data_type != TSDataType.DOUBLE + or self.value is None + or self.value is pd.NA + ): return None return np.float64(self.value) @@ -146,7 +154,8 @@ def get_binary_value(self): self.__data_type != TSDataType.TEXT and self.__data_type != TSDataType.STRING and self.__data_type != TSDataType.BLOB - or _is_missing(self.value) + or self.value is None + or self.value is pd.NA ): return None return self.value @@ -154,21 +163,27 @@ def get_binary_value(self): def get_timestamp_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if self.__data_type != TSDataType.TIMESTAMP or _is_missing(self.value): + if ( + self.__data_type != TSDataType.TIMESTAMP + or self.value is None + or self.value is pd.NA + ): return None - from iotdb.utils.rpc_utils import convert_to_timestamp - return convert_to_timestamp(self.value, self.__precision, self.__timezone) def get_date_value(self): if self.__data_type is None: raise Exception("Null Field Exception!") - if self.__data_type != TSDataType.DATE or _is_missing(self.value): + if ( + self.__data_type != TSDataType.DATE + or self.value is None + or self.value is pd.NA + ): return None return parse_int_to_date(self.value) def get_string_value(self): - if self.__data_type is None or _is_missing(self.value): + if self.__data_type is None or self.value is None or self.value is pd.NA: return "None" # TEXT, STRING if self.__data_type == 5 or self.__data_type == 11: @@ -178,8 +193,6 @@ def get_string_value(self): return str(hex(int.from_bytes(self.value, byteorder="big"))) # TIMESTAMP elif self.__data_type == 8: - from iotdb.utils.rpc_utils import convert_to_timestamp, isoformat - return isoformat( convert_to_timestamp(self.value, self.__precision, self.__timezone), self.__precision, @@ -195,7 +208,7 @@ def get_object_value(self, data_type): """ :param data_type: TSDataType """ - if self.__data_type is None or _is_missing(self.value): + if self.__data_type is None or self.value is None or self.value is pd.NA: return None if data_type == 0: return bool(self.value) @@ -208,8 +221,6 @@ def get_object_value(self, data_type): elif data_type == 4: return np.float64(self.value) elif data_type == 8: - from iotdb.utils.rpc_utils import convert_to_timestamp - return convert_to_timestamp(self.value, self.__precision, self.__timezone) elif data_type == 9: return parse_int_to_date(self.value) @@ -226,7 +237,7 @@ def get_field(value, data_type): :param value: field value corresponding to the data type :param data_type: TSDataType """ - if _is_missing(value): + if value is None or value is pd.NA: return None field = Field(data_type, value) return field diff --git a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py index f3730c64933c7..b0bf291de0cfd 100644 --- a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py +++ b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py @@ -16,8 +16,9 @@ # under the License. # import logging -from typing import TYPE_CHECKING, Optional +from typing import Optional +import pandas as pd from iotdb.utils.Field import Field from iotdb.utils.iotdb_rpc_dataset import IoTDBRpcDataSet @@ -25,9 +26,6 @@ from iotdb.utils.IoTDBConstants import TSDataType from iotdb.utils.RowRecord import RowRecord -if TYPE_CHECKING: - import pandas as pd - logger = logging.getLogger("IoTDB") @@ -148,7 +146,7 @@ def has_next_df(self) -> bool: rpc_ds = self.iotdb_rpc_data_set return rpc_ds._has_buffered_data() or rpc_ds._has_next_result_set() - def next_df(self) -> Optional["pd.DataFrame"]: + def next_df(self) -> Optional[pd.DataFrame]: """ Get the next DataFrame from the result set. Each returned DataFrame contains exactly fetch_size rows, @@ -157,11 +155,11 @@ def next_df(self) -> Optional["pd.DataFrame"]: """ return self.iotdb_rpc_data_set.next_dataframe() - def todf(self) -> "pd.DataFrame": + def todf(self) -> pd.DataFrame: return result_set_to_pandas(self) -def result_set_to_pandas(result_set: SessionDataSet) -> "pd.DataFrame": +def result_set_to_pandas(result_set: SessionDataSet) -> pd.DataFrame: """ Transforms a SessionDataSet from IoTDB to a Pandas Data Frame Each Field from IoTDB is a column in Pandas diff --git a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py index cf43c8bd6f823..289367b6dd7e5 100644 --- a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py +++ b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py @@ -19,19 +19,18 @@ # for package import logging from collections import deque -from typing import TYPE_CHECKING, Optional +from typing import Optional import numpy as np +import pandas as pd from iotdb.thrift.rpc.IClientRPCService import TSCloseOperationReq, TSFetchResultsReq +from iotdb.tsfile.utils.date_utils import parse_int_to_date from iotdb.tsfile.utils.tsblock_serde import deserialize from iotdb.utils.exception import IoTDBConnectionException from iotdb.utils.IoTDBConstants import TSDataType -from iotdb.utils.rpc_utils import verify_success +from iotdb.utils.rpc_utils import convert_to_timestamp, verify_success from thrift.transport import TTransport -if TYPE_CHECKING: - import pandas as pd - logger = logging.getLogger("IoTDB") TIMESTAMP_STR = "Time" @@ -221,8 +220,6 @@ def _expand_column(values, nulls, row_count, data_type): return [None if is_null else next(source_values) for is_null in nulls] def construct_one_data_frame(self): - import pandas as pd - if self.has_cached_data_frame or self.__query_result is None: return result = {} @@ -316,14 +313,12 @@ def _has_buffered_data(self) -> bool: """ return self.__df_buffer is not None and len(self.__df_buffer) > 0 - def next_dataframe(self) -> Optional["pd.DataFrame"]: + def next_dataframe(self) -> Optional[pd.DataFrame]: """ Get the next DataFrame from the result set with exactly fetch_size rows. The last DataFrame may have fewer rows. :return: the next DataFrame with fetch_size rows, or None if no more data """ - import pandas as pd - # Accumulate data until we have at least fetch_size rows or no more data while True: buffer_len = 0 if self.__df_buffer is None else len(self.__df_buffer) @@ -373,10 +368,6 @@ def result_set_to_pandas(self): return self._build_dataframe(result) def _process_buffer(self): - import pandas as pd - from iotdb.tsfile.utils.date_utils import parse_int_to_date - from iotdb.utils.rpc_utils import convert_to_timestamp - result = {} for i in range(len(self.__column_index_2_tsblock_column_index_list)): result[i] = [] @@ -476,8 +467,6 @@ def _process_buffer(self): return result def _build_dataframe(self, result): - import pandas as pd - for k, v in result.items(): if v is None or len(v) < 1 or v[0] is None: result[k] = [] diff --git a/iotdb-client/client-py/iotdb/utils/rpc_utils.py b/iotdb-client/client-py/iotdb/utils/rpc_utils.py index 5eb95d95b954a..0912fd14d95de 100644 --- a/iotdb-client/client-py/iotdb/utils/rpc_utils.py +++ b/iotdb-client/client-py/iotdb/utils/rpc_utils.py @@ -17,6 +17,10 @@ # import logging +import pandas as pd +from pandas._libs import OutOfBoundsDatetime +from tzlocal import get_localzone_name + from iotdb.thrift.common.ttypes import TSStatus from iotdb.utils.exception import RedirectException, StatementExecutionException @@ -74,10 +78,6 @@ def verify_success_with_redirection_for_multi_devices(status: TSStatus, devices: def convert_to_timestamp(time: int, precision: str, timezone: str): - import pandas as pd - from pandas._libs import OutOfBoundsDatetime - from tzlocal import get_localzone_name - try: ts = pd.Timestamp(time, unit=precision, tz=timezone) except OutOfBoundsDatetime: @@ -116,7 +116,7 @@ def convert_to_timestamp(time: int, precision: str, timezone: str): } -def isoformat(ts, unit: str): +def isoformat(ts: pd.Timestamp, unit: str): if unit not in unit_map: raise ValueError(f"Unsupported unit: {unit}") try: @@ -134,7 +134,7 @@ def isoformat(ts, unit: str): return _isoformat_from_components(ts, unit) -def _isoformat_from_components(ts, unit: str) -> str: +def _isoformat_from_components(ts: pd.Timestamp, unit: str) -> str: base = ( f"{ts.year:04d}-{ts.month:02d}-{ts.day:02d}" f"T{ts.hour:02d}:{ts.minute:02d}:{ts.second:02d}" From 71068af94290205dfb4f71958b286b84f65c4d52 Mon Sep 17 00:00:00 2001 From: 761417898 <761417898@qq.com> Date: Tue, 15 Sep 2026 18:00:02 +0800 Subject: [PATCH 5/5] Stream decoded columns into row tuples --- .../client-py/iotdb/utils/SessionDataSet.py | 7 +++++-- .../client-py/iotdb/utils/iotdb_rpc_dataset.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py index b0bf291de0cfd..3f92c78fb6a48 100644 --- a/iotdb-client/client-py/iotdb/utils/SessionDataSet.py +++ b/iotdb-client/client-py/iotdb/utils/SessionDataSet.py @@ -110,9 +110,9 @@ def has_next(self): def next(self): if not self.has_next(): return None - return self.construct_row_record_from_data_frame() + return self._construct_row_record() - def construct_row_record_from_data_frame(self): + def _construct_row_record(self): row = self.iotdb_rpc_data_set._pop_row() if row is None: return None @@ -134,6 +134,9 @@ def construct_row_record_from_data_frame(self): ) return row_record + def construct_row_record_from_data_frame(self): + return self._construct_row_record() + def close_operation_handle(self): self.iotdb_rpc_data_set.close() diff --git a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py index 289367b6dd7e5..bcb259c6f4263 100644 --- a/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py +++ b/iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py @@ -193,12 +193,12 @@ def _deserialize_rows(self, serialized_block): ) columns = [] if not self.ignore_timestamp: - columns.append(time_array.tolist()) + columns.append(iter(time_array.tolist())) for location in self.__column_index_2_tsblock_column_index_list: if location < 0: continue columns.append( - self._expand_column( + self._column_values( column_arrays[location], null_indicators[location], row_count, @@ -208,16 +208,16 @@ def _deserialize_rows(self, serialized_block): return list(zip(*columns)) if columns else [tuple() for _ in range(row_count)] @staticmethod - def _expand_column(values, nulls, row_count, data_type): + def _column_values(values, nulls, row_count, data_type): source = values.tolist() if hasattr(values, "tolist") else list(values) if nulls is None: - return source + return iter(source) if data_type == TSDataType.BOOLEAN and len(source) == row_count: - return [ - None if nulls[index] else source[index] for index in range(row_count) - ] + return ( + None if nulls[index] else value for index, value in enumerate(source) + ) source_values = iter(source) - return [None if is_null else next(source_values) for is_null in nulls] + return (None if is_null else next(source_values) for is_null in nulls) def construct_one_data_frame(self): if self.has_cached_data_frame or self.__query_result is None: