From 07e1b40171ffb3c961dfc2fb7c4feda886067ae7 Mon Sep 17 00:00:00 2001 From: ColinLee Date: Mon, 20 Jul 2026 17:43:13 +0800 Subject: [PATCH 1/8] fix(python): align native error handling --- cpp/src/cwrapper/errno_define_c.h | 67 ++-- cpp/test/cwrapper/c_release_test.cc | 3 +- python/tests/test_exceptions.py | 106 ++++++ python/tests/test_write_and_read.py | 8 +- python/tsfile/exceptions.py | 163 ++++++++- python/tsfile/tsfile_cpp.pxd | 5 + python/tsfile/tsfile_py_cpp.pxd | 4 +- python/tsfile/tsfile_py_cpp.pyx | 550 +++++++++++++++++++--------- python/tsfile/tsfile_reader.pyx | 4 +- python/tsfile/utils.py | 8 +- 10 files changed, 712 insertions(+), 206 deletions(-) create mode 100644 python/tests/test_exceptions.py diff --git a/cpp/src/cwrapper/errno_define_c.h b/cpp/src/cwrapper/errno_define_c.h index 3ceb06416..af9cd1ca1 100644 --- a/cpp/src/cwrapper/errno_define_c.h +++ b/cpp/src/cwrapper/errno_define_c.h @@ -17,8 +17,8 @@ * under the License. */ -#ifndef CWRAPPER_ERRNO_DEFINRET_H -#define CWRAPPER_ERRNO_DEFINRET_H +#ifndef CWRAPPER_ERRNO_DEFINE_H +#define CWRAPPER_ERRNO_DEFINE_H #define RET_OK 0 #define RET_OOM 1 @@ -35,42 +35,65 @@ #define RET_NET_FCNTL_ERR 13 #define RET_NET_LISTEN_ERR 14 #define RET_NET_SEND_ERR 15 -#define RET_PIPRET_ERR 16 -#define RET_THREAD_CREATRET_ERR 17 +#define RET_PIPE_ERR 16 +#define RET_THREAD_CREATE_ERR 17 #define RET_MUTEX_ERR 18 #define RET_COND_ERR 19 #define RET_OVERFLOW 20 -#define RET_NO_MORRET_DATA 21 +#define RET_NO_MORE_DATA 21 #define RET_OUT_OF_ORDER 22 -#define RET_TSBLOCK_TYPRET_NOT_SUPPORTED 23 +#define RET_TSBLOCK_TYPE_NOT_SUPPORTED 23 #define RET_TSBLOCK_DATA_INCONSISTENCY 24 #define RET_DDL_UNKNOWN_TYPE 25 -#define RET_TYPRET_NOT_SUPPORTED 26 -#define RET_TYPRET_NOT_MATCH 27 -#define RET_FILRET_OPEN_ERR 28 -#define RET_FILRET_CLOSRET_ERR 29 -#define RET_FILRET_WRITRET_ERR 30 -#define RET_FILRET_READ_ERR 31 -#define RET_FILRET_SYNC_ERR 32 -#define RET_TSFILRET_WRITER_META_ERR 33 -#define RET_FILRET_STAT_ERR 34 -#define RET_TSFILRET_CORRUPTED 35 +#define RET_TYPE_NOT_SUPPORTED 26 +#define RET_TYPE_NOT_MATCH 27 +#define RET_FILE_OPEN_ERR 28 +#define RET_FILE_CLOSE_ERR 29 +#define RET_FILE_WRITE_ERR 30 +#define RET_FILE_READ_ERR 31 +#define RET_FILE_SYNC_ERR 32 +#define RET_TSFILE_WRITER_META_ERR 33 +#define RET_FILE_STAT_ERR 34 +#define RET_TSFILE_CORRUPTED 35 #define RET_BUF_NOT_ENOUGH 36 #define RET_INVALID_PATH 37 #define RET_NOT_MATCH 38 #define RET_JSON_INVALID 39 #define RET_NOT_SUPPORT 40 #define RET_PARSER_ERR 41 -#define RET_ANALYZRET_ERR 42 +#define RET_ANALYZE_ERR 42 #define RET_INVALID_DATA_POINT 43 -#define RET_DEVICRET_NOT_EXIST 44 +#define RET_DEVICE_NOT_EXIST 44 #define RET_MEASUREMENT_NOT_EXIST 45 #define RET_INVALID_QUERY 46 -#define RET_SDK_QUERY_OPTIMIZRET_ERR 47 +#define RET_SDK_QUERY_OPTIMIZE_ERR 47 #define RET_COMPRESS_ERR 48 -#define RET_TABLRET_NOT_EXIST 49 +#define RET_TABLE_NOT_EXIST 49 #define RET_COLUMN_NOT_EXIST 50 #define RET_UNSUPPORTED_ORDER 51 -#define RET_INVALID_NODRET_TYPE 52 +#define RET_INVALID_NODE_TYPE 52 +#define RET_ENCODE_ERR 53 +#define RET_DECODE_ERR 54 -#endif /* CWRAPPER_ERRNO_DEFINRET_H */ \ No newline at end of file +/* Backward-compatible aliases for identifiers published with misspellings. */ +#define RET_PIPRET_ERR RET_PIPE_ERR +#define RET_THREAD_CREATRET_ERR RET_THREAD_CREATE_ERR +#define RET_NO_MORRET_DATA RET_NO_MORE_DATA +#define RET_TSBLOCK_TYPRET_NOT_SUPPORTED RET_TSBLOCK_TYPE_NOT_SUPPORTED +#define RET_TYPRET_NOT_SUPPORTED RET_TYPE_NOT_SUPPORTED +#define RET_TYPRET_NOT_MATCH RET_TYPE_NOT_MATCH +#define RET_FILRET_OPEN_ERR RET_FILE_OPEN_ERR +#define RET_FILRET_CLOSRET_ERR RET_FILE_CLOSE_ERR +#define RET_FILRET_WRITRET_ERR RET_FILE_WRITE_ERR +#define RET_FILRET_READ_ERR RET_FILE_READ_ERR +#define RET_FILRET_SYNC_ERR RET_FILE_SYNC_ERR +#define RET_TSFILRET_WRITER_META_ERR RET_TSFILE_WRITER_META_ERR +#define RET_FILRET_STAT_ERR RET_FILE_STAT_ERR +#define RET_TSFILRET_CORRUPTED RET_TSFILE_CORRUPTED +#define RET_ANALYZRET_ERR RET_ANALYZE_ERR +#define RET_DEVICRET_NOT_EXIST RET_DEVICE_NOT_EXIST +#define RET_SDK_QUERY_OPTIMIZRET_ERR RET_SDK_QUERY_OPTIMIZE_ERR +#define RET_TABLRET_NOT_EXIST RET_TABLE_NOT_EXIST +#define RET_INVALID_NODRET_TYPE RET_INVALID_NODE_TYPE + +#endif /* CWRAPPER_ERRNO_DEFINE_H */ diff --git a/cpp/test/cwrapper/c_release_test.cc b/cpp/test/cwrapper/c_release_test.cc index bb21483f7..f27e049e0 100644 --- a/cpp/test/cwrapper/c_release_test.cc +++ b/cpp/test/cwrapper/c_release_test.cc @@ -54,8 +54,7 @@ TEST_F(CReleaseTest, TestCreateFile) { // Folder: rejected either as an open error (POSIX) or as already-existing // (Windows / filesystems where the directory already exists). file = write_file_new("test/", &error_no); - ASSERT_TRUE(error_no == RET_FILRET_OPEN_ERR || - error_no == RET_ALREADY_EXIST); + ASSERT_TRUE(error_no == RET_FILE_OPEN_ERR || error_no == RET_ALREADY_EXIST); remove("create_file1.tsfile"); free_write_file(&file); diff --git a/python/tests/test_exceptions.py b/python/tests/test_exceptions.py new file mode 100644 index 000000000..7d025dc27 --- /dev/null +++ b/python/tests/test_exceptions.py @@ -0,0 +1,106 @@ +# 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 pytest + +from tsfile import Field, RowRecord, Tablet, TimeseriesSchema, TSDataType +from tsfile import TsFileReader, TsFileWriter +from tsfile.exceptions import ( + AlreadyExistsError, + ErrorCode, + FileOpenError, + InvalidArgumentError, + LibraryError, + TsFileCorruptedError, + get_exception, +) + + +def test_get_exception_preserves_known_and_unknown_codes(): + corrupted = get_exception(ErrorCode.TSFILE_CORRUPTED) + assert isinstance(corrupted, TsFileCorruptedError) + assert corrupted.code == ErrorCode.TSFILE_CORRUPTED + assert corrupted.message == "TsFile is corrupted" + + unknown = get_exception(999, "opening reader") + assert type(unknown) is LibraryError + assert unknown.code == 999 + assert unknown.message == "opening reader: Unknown library error" + + +def test_writer_constructor_propagates_native_error(tmp_path, capsys): + path = tmp_path / "already-exists.tsfile" + path.touch() + + with pytest.raises(AlreadyExistsError) as exc_info: + TsFileWriter(str(path)) + + assert exc_info.value.code == ErrorCode.ALREADY_EXIST + assert exc_info.value.message == "Resource already exists" + assert capsys.readouterr().out == "" + + +def test_reader_constructor_propagates_native_errors(tmp_path): + missing = tmp_path / "missing.tsfile" + with pytest.raises(FileOpenError) as exc_info: + TsFileReader(str(missing)) + assert exc_info.value.code == ErrorCode.FILE_OPEN_ERROR + + corrupted = tmp_path / "corrupted.tsfile" + corrupted.touch() + with pytest.raises(TsFileCorruptedError) as exc_info: + TsFileReader(str(corrupted)) + assert exc_info.value.code == ErrorCode.TSFILE_CORRUPTED + + +def test_duplicate_tablet_columns_do_not_silently_drop_rows(tmp_path): + path = tmp_path / "duplicate-columns.tsfile" + with TsFileWriter(str(path)) as writer: + writer.register_timeseries( + "root.device", TimeseriesSchema("value", TSDataType.INT64) + ) + tablet = Tablet( + ["value", "VALUE"], + [TSDataType.INT64, TSDataType.INT64], + max_row_num=1, + ) + tablet.set_table_name("root.device") + tablet.add_timestamp(0, 1) + tablet.add_value_by_index(0, 0, 10) + tablet.add_value_by_index(1, 0, 20) + + with pytest.raises(InvalidArgumentError) as exc_info: + writer.write_tablet(tablet) + + assert exc_info.value.code == ErrorCode.INVALID_ARGUMENT + assert "unique" in exc_info.value.message + + +def test_row_record_still_writes_after_error_handling_changes(tmp_path): + path = tmp_path / "row-record.tsfile" + with TsFileWriter(str(path)) as writer: + writer.register_timeseries( + "root.device", TimeseriesSchema("value", TSDataType.INT64) + ) + writer.write_row_record( + RowRecord("root.device", 1, [Field("value", 10, TSDataType.INT64)]) + ) + + with TsFileReader(str(path)) as reader: + with reader.query_timeseries("root.device", ["value"], 0, 2) as result: + assert result.next() + assert result.get_value_by_index(2) == 10 diff --git a/python/tests/test_write_and_read.py b/python/tests/test_write_and_read.py index a1b3b609e..fda145efd 100644 --- a/python/tests/test_write_and_read.py +++ b/python/tests/test_write_and_read.py @@ -639,10 +639,14 @@ def test_tsfile_to_df(): max_row_num=8000, ) assert df3.shape == (4097, 3) - with pytest.raises(TableNotExistError): + with pytest.raises(TableNotExistError) as table_error: to_dataframe("table_write_to_df.tsfile", "test_tb") - with pytest.raises(ColumnNotExistError): + assert table_error.value.code == 49 + assert "test_tb" in table_error.value.message + with pytest.raises(ColumnNotExistError) as column_error: to_dataframe("table_write_to_df.tsfile", "test_table", ["device1"]) + assert column_error.value.code == 50 + assert "device1" in column_error.value.message finally: os.remove("table_write_to_df.tsfile") diff --git a/python/tsfile/exceptions.py b/python/tsfile/exceptions.py index a2e930bad..8f355c21c 100644 --- a/python/tsfile/exceptions.py +++ b/python/tsfile/exceptions.py @@ -17,6 +17,66 @@ # +from enum import IntEnum + + +class ErrorCode(IntEnum): + OK = 0 + OOM = 1 + NOT_EXIST = 2 + ALREADY_EXIST = 3 + INVALID_ARGUMENT = 4 + OUT_OF_RANGE = 5 + PARTIAL_READ = 6 + INVALID_SCHEMA = 8 + NET_EPOLL_ERROR = 9 + NET_EPOLL_WAIT_ERROR = 10 + NET_RECEIVE_ERROR = 11 + NET_ACCEPT_ERROR = 12 + NET_FCNTL_ERROR = 13 + NET_LISTEN_ERROR = 14 + NET_SEND_ERROR = 15 + PIPE_ERROR = 16 + THREAD_CREATE_ERROR = 17 + MUTEX_ERROR = 18 + CONDITION_ERROR = 19 + OVERFLOW = 20 + NO_MORE_DATA = 21 + OUT_OF_ORDER = 22 + TSBLOCK_TYPE_NOT_SUPPORTED = 23 + DATA_INCONSISTENCY = 24 + DDL_UNKNOWN_TYPE = 25 + TYPE_NOT_SUPPORTED = 26 + TYPE_MISMATCH = 27 + FILE_OPEN_ERROR = 28 + FILE_CLOSE_ERROR = 29 + FILE_WRITE_ERROR = 30 + FILE_READ_ERROR = 31 + FILE_SYNC_ERROR = 32 + WRITER_METADATA_ERROR = 33 + FILE_STAT_ERROR = 34 + TSFILE_CORRUPTED = 35 + BUFFER_NOT_ENOUGH = 36 + INVALID_PATH = 37 + NOT_MATCH = 38 + JSON_INVALID = 39 + NOT_SUPPORTED = 40 + PARSER_ERROR = 41 + ANALYZE_ERROR = 42 + INVALID_DATA_POINT = 43 + DEVICE_NOT_EXIST = 44 + MEASUREMENT_NOT_EXIST = 45 + INVALID_QUERY = 46 + QUERY_OPTIMIZE_ERROR = 47 + COMPRESSION_ERROR = 48 + TABLE_NOT_EXIST = 49 + COLUMN_NOT_EXIST = 50 + UNSUPPORTED_ORDER = 51 + INVALID_NODE_TYPE = 52 + ENCODE_ERROR = 53 + DECODE_ERROR = 54 + + class LibraryError(Exception): _default_message = "Unknown error occurred" _default_code = -1 @@ -60,6 +120,31 @@ class PartialReadError(LibraryError): _default_code = 6 +class InvalidSchemaError(LibraryError): + _default_message = "Invalid schema" + _default_code = 8 + + +class TsFileOverflowError(LibraryError): + _default_message = "Buffer or value overflow" + _default_code = 20 + + +class NoMoreDataError(LibraryError): + _default_message = "No more data" + _default_code = 21 + + +class OutOfOrderError(LibraryError): + _default_message = "Data is out of order" + _default_code = 22 + + +class DataInconsistencyError(LibraryError): + _default_message = "Data is inconsistent" + _default_code = 24 + + class FileOpenError(LibraryError): _default_message = "Failed to open file" _default_code = 28 @@ -90,11 +175,26 @@ class MetadataError(LibraryError): _default_code = 33 +class FileStatError(LibraryError): + _default_message = "Failed to inspect file metadata" + _default_code = 34 + + +class TsFileCorruptedError(LibraryError): + _default_message = "TsFile is corrupted" + _default_code = 35 + + class BufferNotEnoughError(LibraryError): _default_message = "Insufficient buffer space" _default_code = 36 +class InvalidPathError(LibraryError): + _default_message = "Invalid path" + _default_code = 37 + + class NotSupportedError(LibraryError): _default_message = "Not support yet" _default_code = 40 @@ -115,6 +215,11 @@ class InvalidQueryError(LibraryError): _default_code = 46 +class QueryOptimizeError(LibraryError): + _default_message = "Failed to optimize query" + _default_code = 47 + + class CompressionError(LibraryError): _default_message = "Data compression/decompression failed" _default_code = 48 @@ -140,6 +245,26 @@ class ColumnNotExistError(LibraryError): _default_code = 50 +class UnsupportedOrderError(LibraryError): + _default_message = "Unsupported ordering" + _default_code = 51 + + +class InvalidNodeTypeError(LibraryError): + _default_message = "Invalid node type" + _default_code = 52 + + +class EncodeError(LibraryError): + _default_message = "Failed to encode data" + _default_code = 53 + + +class DecodeError(LibraryError): + _default_message = "Failed to decode data" + _default_code = 54 + + ERROR_MAPPING = { 1: OOMError, 2: NotExistsError, @@ -147,6 +272,11 @@ class ColumnNotExistError(LibraryError): 4: InvalidArgumentError, 5: OutOfRangeError, 6: PartialReadError, + 8: InvalidSchemaError, + 20: TsFileOverflowError, + 21: NoMoreDataError, + 22: OutOfOrderError, + 24: DataInconsistencyError, 26: TypeNotSupportedError, 27: TypeMismatchError, 28: FileOpenError, @@ -155,24 +285,43 @@ class ColumnNotExistError(LibraryError): 31: FileReadError, 32: FileSyncError, 33: MetadataError, + 34: FileStatError, + 35: TsFileCorruptedError, 36: BufferNotEnoughError, + 37: InvalidPathError, 40: NotSupportedError, 44: DeviceNotExistError, 45: MeasurementNotExistError, 46: InvalidQueryError, + 47: QueryOptimizeError, 48: CompressionError, 49: TableNotExistError, 50: ColumnNotExistError, + 51: UnsupportedOrderError, + 52: InvalidNodeTypeError, + 53: EncodeError, + 54: DecodeError, +} + + +ERROR_MESSAGES = { + error.value: error.name.replace("_", " ").lower() for error in ErrorCode } +ERROR_MESSAGES.update( + { + code: exception_type._default_message + for code, exception_type in ERROR_MAPPING.items() + } +) def get_exception(code: int, context: str = None): - if code == 0: + code = int(code) + if code == ErrorCode.OK: return None - exc_type = ERROR_MAPPING.get(code) - if not exc_type: - return LibraryError( - code=code, context=f"Unmapped error code: {code}, message: {context}" - ) - return exc_type(code=code, context=context) + exc_type = ERROR_MAPPING.get(code, LibraryError) + message = ERROR_MESSAGES.get(code, "Unknown library error") + if context: + message = f"{context}: {message}" + return exc_type(code=code, context=message) diff --git a/python/tsfile/tsfile_cpp.pxd b/python/tsfile/tsfile_cpp.pxd index 4e90dd483..cc14f4034 100644 --- a/python/tsfile/tsfile_cpp.pxd +++ b/python/tsfile/tsfile_cpp.pxd @@ -21,6 +21,11 @@ from libc.stdint cimport uint32_t, int32_t, int64_t, uint64_t, uint8_t ctypedef int32_t ErrorCode +cdef extern from "cwrapper/errno_define_c.h": + enum: + RET_OK + RET_NO_MORE_DATA + # import symbols from tsfile_cwrapper.h cdef extern from "cwrapper/tsfile_cwrapper.h": # common diff --git a/python/tsfile/tsfile_py_cpp.pxd b/python/tsfile/tsfile_py_cpp.pxd index 7286e78fb..0e2f91cbc 100644 --- a/python/tsfile/tsfile_py_cpp.pxd +++ b/python/tsfile/tsfile_py_cpp.pxd @@ -41,8 +41,8 @@ cdef public api void free_c_timeseries_schema(TimeseriesSchema* c_schema) cdef public api void free_c_device_schema(DeviceSchema* c_schema) cdef public api void free_c_tablet(Tablet tablet) cdef public api void free_c_row_record(TsRecord record) -cdef public api TsFileWriter tsfile_writer_new_c(object pathname, uint64_t memory_threshold) except + -cdef public api TsFileReader tsfile_reader_new_c(object pathname) except + +cdef public api TsFileWriter tsfile_writer_new_c(object pathname, uint64_t memory_threshold) except NULL +cdef public api TsFileReader tsfile_reader_new_c(object pathname) except NULL cdef public api ErrorCode tsfile_writer_register_device_py_cpp(TsFileWriter writer, DeviceSchema *schema) cdef public api ErrorCode tsfile_writer_register_timeseries_py_cpp(TsFileWriter writer, object device_name, TimeseriesSchema *schema) diff --git a/python/tsfile/tsfile_py_cpp.pyx b/python/tsfile/tsfile_py_cpp.pyx index 038336a42..e36b548ac 100644 --- a/python/tsfile/tsfile_py_cpp.pyx +++ b/python/tsfile/tsfile_py_cpp.pyx @@ -27,11 +27,10 @@ from libc.stdlib cimport free from libc.stdlib cimport malloc from libc.string cimport strdup from libc.string cimport memset -from cpython.exc cimport PyErr_SetObject from cpython.unicode cimport PyUnicode_AsUTF8String, PyUnicode_AsUTF8, PyUnicode_AsUTF8AndSize from cpython.bytes cimport PyBytes_AsString, PyBytes_AsStringAndSize -from tsfile.exceptions import ERROR_MAPPING, TypeMismatchError +from tsfile.exceptions import get_exception, InvalidArgumentError, TypeMismatchError from tsfile.schema import ResultSetMetaData as ResultSetMetaDataPy from tsfile.schema import TSDataType as TSDataTypePy, TSEncoding as TSEncodingPy from tsfile.schema import Compressor as CompressorPy, ColumnCategory as CategoryPy @@ -49,17 +48,11 @@ from tsfile.schema import TimeseriesMetadata as TimeseriesMetadataPy # check exception and set py exception object cdef inline void check_error(int errcode, const char * context=NULL) except*: - cdef: - object exc_type - object exc_instance - - if errcode == 0: + if errcode == RET_OK: return - exc_type = ERROR_MAPPING.get(errcode) - print(exc_type) - exc_instance = exc_type(errcode, "") - PyErr_SetObject(exc_type, exc_instance) + py_context = context.decode('utf-8') if context != NULL else None + raise get_exception(errcode, py_context) # convert from c to python cdef object from_c_result_set_meta_data(ResultSetMetaData schema): @@ -226,18 +219,35 @@ cdef TableSchema * to_c_table_schema(object py_schema): return c_schema cdef Tablet to_c_tablet(object tablet): - cdef Tablet ctablet + cdef Tablet ctablet = NULL cdef int max_row_num + cdef int column_num + cdef int i, row, col cdef TSDataType data_type cdef int64_t timestamp cdef bytes device_id_bytes cdef const char * device_id_c - cdef char** columns_names - cdef TSDataType * column_types - cdef bytes row_bytes - cdef char *raw_str + cdef char** columns_names = NULL + cdef TSDataType * columns_types = NULL + cdef char *raw_str = NULL cdef const char * str_ptr cdef Py_ssize_t raw_len + cdef object column_name_list = tablet.get_column_name_list() + cdef object data_type_list = tablet.get_data_type_list() + cdef object value_list = tablet.get_value_list() + + column_num = len(column_name_list) + max_row_num = tablet.get_max_row_num() + if column_num != len(data_type_list) or column_num != len(value_list): + raise InvalidArgumentError( + context="Tablet column names, data types, and values must have equal lengths" + ) + if len({( name).lower() for name in column_name_list}) != column_num: + raise InvalidArgumentError( + context="Tablet column names must be unique (case-insensitive)" + ) + if max_row_num <= 0: + raise InvalidArgumentError(context="Tablet max_row_num must be positive") if tablet.get_target_name() is not None: device_id_bytes = PyUnicode_AsUTF8String(tablet.get_target_name()) @@ -245,90 +255,153 @@ cdef Tablet to_c_tablet(object tablet): else: device_id_c = NULL - column_num = len(tablet.get_column_name_list()) columns_names = malloc(sizeof(char *) * column_num) columns_types = malloc(sizeof(TSDataType) * column_num) - for i in range(column_num): - columns_names[i] = strdup(tablet.get_column_name_list()[i].encode('utf-8')) - columns_types[i] = to_c_data_type(tablet.get_data_type_list()[i]) - - max_row_num = tablet.get_max_row_num() - - ctablet = _tablet_new_with_target_name(device_id_c, columns_names, columns_types, column_num, - max_row_num) - free(columns_types) - for i in range(column_num): - free(columns_names[i]) - free(columns_names) - - for row in range(max_row_num): - timestamp_py = tablet.get_timestamp_list()[row] - if timestamp_py is None: - continue - timestamp = timestamp_py - tablet_add_timestamp(ctablet, row, timestamp) - - for col in range(column_num): - data_type = to_c_data_type(tablet.get_data_type_list()[col]) - value = tablet.get_value_list()[col] - # BOOLEAN - if data_type == TS_DATATYPE_BOOLEAN: - for row in range(max_row_num): - if value[row] is not None: - tablet_add_value_by_index_bool(ctablet, row, col, value[row]) - # INT32 - elif data_type == TS_DATATYPE_INT32: - for row in range(max_row_num): - if value[row] is not None: - tablet_add_value_by_index_int32_t(ctablet, row, col, value[row]) - - # INT64 - elif data_type == TS_DATATYPE_INT64 or data_type == TS_DATATYPE_TIMESTAMP: - for row in range(max_row_num): - if value[row] is not None: - tablet_add_value_by_index_int64_t(ctablet, row, col, value[row]) - # FLOAT - elif data_type == TS_DATATYPE_FLOAT: - for row in range(max_row_num): - if value[row] is not None: - tablet_add_value_by_index_float(ctablet, row, col, value[row]) + if columns_names == NULL or columns_types == NULL: + free(columns_names) + free(columns_types) + raise MemoryError("Failed to allocate tablet schema arrays") + memset(columns_names, 0, sizeof(char *) * column_num) + try: + for i in range(column_num): + columns_names[i] = strdup(( column_name_list[i]).encode('utf-8')) + if columns_names[i] == NULL: + raise MemoryError("Failed to allocate tablet column name") + columns_types[i] = to_c_data_type(data_type_list[i]) + ctablet = _tablet_new_with_target_name( + device_id_c, columns_names, columns_types, column_num, max_row_num + ) + finally: + free(columns_types) + for i in range(column_num): + free(columns_names[i]) + free(columns_names) - # DOUBLE - elif data_type == TS_DATATYPE_DOUBLE: - for row in range(max_row_num): - if value[row] is not None: - tablet_add_value_by_index_double(ctablet, row, col, value[row]) + if ctablet == NULL: + raise MemoryError("Failed to allocate native tablet") - elif data_type == TS_DATATYPE_DATE: - for row in range(max_row_num): - if value[row] is not None: - tablet_add_value_by_index_int32_t(ctablet, row, col, parse_date_to_int(value[row])) + try: + for row in range(max_row_num): + timestamp_py = tablet.get_timestamp_list()[row] + if timestamp_py is None: + continue + timestamp = timestamp_py + check_error( + tablet_add_timestamp(ctablet, row, timestamp), + b"Failed to add tablet timestamp", + ) + + for col in range(column_num): + data_type = to_c_data_type(data_type_list[col]) + value = value_list[col] + # BOOLEAN + if data_type == TS_DATATYPE_BOOLEAN: + for row in range(max_row_num): + if value[row] is not None: + check_error( + tablet_add_value_by_index_bool( + ctablet, row, col, value[row] + ), + b"Failed to add BOOLEAN tablet value", + ) + # INT32 + elif data_type == TS_DATATYPE_INT32: + for row in range(max_row_num): + if value[row] is not None: + check_error( + tablet_add_value_by_index_int32_t( + ctablet, row, col, value[row] + ), + b"Failed to add INT32 tablet value", + ) + # INT64 + elif data_type == TS_DATATYPE_INT64 or data_type == TS_DATATYPE_TIMESTAMP: + for row in range(max_row_num): + if value[row] is not None: + check_error( + tablet_add_value_by_index_int64_t( + ctablet, row, col, value[row] + ), + b"Failed to add INT64 tablet value", + ) + # FLOAT + elif data_type == TS_DATATYPE_FLOAT: + for row in range(max_row_num): + if value[row] is not None: + check_error( + tablet_add_value_by_index_float( + ctablet, row, col, value[row] + ), + b"Failed to add FLOAT tablet value", + ) + # DOUBLE + elif data_type == TS_DATATYPE_DOUBLE: + for row in range(max_row_num): + if value[row] is not None: + check_error( + tablet_add_value_by_index_double( + ctablet, row, col, value[row] + ), + b"Failed to add DOUBLE tablet value", + ) + elif data_type == TS_DATATYPE_DATE: + for row in range(max_row_num): + if value[row] is not None: + check_error( + tablet_add_value_by_index_int32_t( + ctablet, row, col, parse_date_to_int(value[row]) + ), + b"Failed to add DATE tablet value", + ) + # STRING or TEXT + elif data_type == TS_DATATYPE_STRING or data_type == TS_DATATYPE_TEXT: + for row in range(max_row_num): + if value[row] is not None: + py_value = value[row] + str_ptr = PyUnicode_AsUTF8AndSize(py_value, &raw_len) + check_error( + tablet_add_value_by_index_string_with_len( + ctablet, row, col, str_ptr, raw_len + ), + b"Failed to add STRING tablet value", + ) + elif data_type == TS_DATATYPE_BLOB: + for row in range(max_row_num): + if value[row] is not None: + if PyBytes_AsStringAndSize( + value[row], &raw_str, &raw_len + ) < 0: + raise TypeError("BLOB tablet values must be bytes") + check_error( + tablet_add_value_by_index_string_with_len( + ctablet, row, col, raw_str, raw_len + ), + b"Failed to add BLOB tablet value", + ) + except: + free_tablet(&ctablet) + raise - # STRING or TEXT - elif data_type == TS_DATATYPE_STRING or data_type == TS_DATATYPE_TEXT: - for row in range(max_row_num): - if value[row] is not None: - py_value = value[row] - str_ptr = PyUnicode_AsUTF8AndSize(py_value, &raw_len) - tablet_add_value_by_index_string_with_len(ctablet, row, col, str_ptr, raw_len) + return ctablet - elif data_type == TS_DATATYPE_BLOB: - for row in range(max_row_num): - if value[row] is not None: - PyBytes_AsStringAndSize(value[row], &raw_str, &raw_len) - tablet_add_value_by_index_string_with_len(ctablet, row, col, raw_str, raw_len) +cdef inline void check_dataframe_tablet_error( + Tablet tablet, int errcode, const char * context +) except *: + if errcode == RET_OK: + return + free_tablet(&tablet) + check_error(errcode, context) - return ctablet cdef Tablet dataframe_to_c_tablet(object target_name, object dataframe, object table_schema): - cdef Tablet ctablet + cdef Tablet ctablet = NULL cdef int max_row_num cdef TSDataType data_type cdef int64_t timestamp cdef const char * device_id_c = NULL - cdef char** columns_names - cdef TSDataType * columns_types - cdef char *raw_str + cdef char** columns_names = NULL + cdef TSDataType * columns_types = NULL + cdef char *raw_str = NULL cdef const char * str_ptr cdef Py_ssize_t raw_len cdef int column_num @@ -359,20 +432,68 @@ cdef Tablet dataframe_to_c_tablet(object target_name, object dataframe, object t data_type = table_schema.get_column(column).get_data_type() column_types_list.append(data_type) + # Validate object-backed columns before allocating the native Tablet so a + # validation exception cannot leak the native allocation. + for col in range(column_num): + col_name = data_columns[col] + data_type = column_types_list[col] + if data_type in ( + TS_DATATYPE_DATE, + TS_DATATYPE_STRING, + TS_DATATYPE_TEXT, + TS_DATATYPE_BLOB, + ): + col_series = dataframe[col_name] + first_valid_idx = col_series.first_valid_index() + if first_valid_idx is not None: + value = col_series[first_valid_idx] + if data_type == TS_DATATYPE_DATE and not isinstance(value, date_type): + raise TypeMismatchError( + context=( + f"Column '{col_name}': expected DATE (datetime.date), " + f"got {type(value).__name__}: {value!r}" + ) + ) + if data_type in (TS_DATATYPE_STRING, TS_DATATYPE_TEXT) and not isinstance(value, str): + raise TypeMismatchError( + context=( + f"Column '{col_name}': expected STRING/TEXT, " + f"got {type(value).__name__}: {value!r}" + ) + ) + if data_type == TS_DATATYPE_BLOB and not isinstance(value, bytes): + raise TypeMismatchError( + context=( + f"Column '{col_name}': expected BLOB (bytes), " + f"got {type(value).__name__}: {value!r}" + ) + ) + columns_names = malloc(sizeof(char *) * column_num) columns_types = malloc(sizeof(TSDataType) * column_num) + if columns_names == NULL or columns_types == NULL: + free(columns_names) + free(columns_types) + raise MemoryError("Failed to allocate DataFrame tablet schema arrays") + memset(columns_names, 0, sizeof(char *) * column_num) + try: + for i in range(column_num): + columns_names[i] = strdup(data_columns[i].lower().encode('utf-8')) + if columns_names[i] == NULL: + raise MemoryError("Failed to allocate DataFrame tablet column name") + columns_types[i] = to_c_data_type(column_types_list[i]) - for i in range(column_num): - columns_names[i] = strdup(data_columns[i].lower().encode('utf-8')) - columns_types[i] = column_types_list[i] - - ctablet = _tablet_new_with_target_name(device_id_c, columns_names, columns_types, column_num, - max_row_num) + ctablet = _tablet_new_with_target_name( + device_id_c, columns_names, columns_types, column_num, max_row_num + ) + finally: + free(columns_types) + for i in range(column_num): + free(columns_names[i]) + free(columns_names) - free(columns_types) - for i in range(column_num): - free(columns_names[i]) - free(columns_names) + if ctablet == NULL: + raise MemoryError("Failed to allocate native DataFrame tablet") if use_id_as_time: for row in range(max_row_num): @@ -380,7 +501,11 @@ cdef Tablet dataframe_to_c_tablet(object target_name, object dataframe, object t if pd.isna(timestamp_py): continue timestamp = timestamp_py - tablet_add_timestamp(ctablet, row, timestamp) + check_dataframe_tablet_error( + ctablet, + tablet_add_timestamp(ctablet, row, timestamp), + b"Failed to add DataFrame index timestamp", + ) else: time_values = dataframe[time_column.get_column_name()].values for row in range(max_row_num): @@ -388,74 +513,89 @@ cdef Tablet dataframe_to_c_tablet(object target_name, object dataframe, object t if pd.isna(timestamp_py): continue timestamp = timestamp_py - tablet_add_timestamp(ctablet, row, timestamp) + check_dataframe_tablet_error( + ctablet, + tablet_add_timestamp(ctablet, row, timestamp), + b"Failed to add DataFrame timestamp", + ) for col in range(column_num): col_name = data_columns[col] data_type = column_types_list[col] column_values = dataframe[col_name].values - # Per-column validation for object types (check first non-null value only) - if data_type in (TS_DATATYPE_DATE, TS_DATATYPE_STRING, TS_DATATYPE_TEXT, TS_DATATYPE_BLOB): - col_series = dataframe[col_name] - first_valid_idx = col_series.first_valid_index() - if first_valid_idx is not None: - value = col_series[first_valid_idx] - if data_type == TS_DATATYPE_DATE: - if not isinstance(value, date_type): - raise TypeMismatchError(context= - f"Column '{col_name}': expected DATE (datetime.date), " - f"got {type(value).__name__}: {value!r}" - ) - elif data_type in (TS_DATATYPE_STRING, TS_DATATYPE_TEXT): - if not isinstance(value, str): - raise TypeMismatchError(context= - f"Column '{col_name}': expected STRING/TEXT, " - f"got {type(value).__name__}: {value!r}" - ) - elif data_type == TS_DATATYPE_BLOB: - if not isinstance(value, bytes): - raise TypeMismatchError(context= - f"Column '{col_name}': expected BLOB (bytes or bytearray), " - f"got {type(value).__name__}: {value!r}" - ) - # BOOLEAN if data_type == TS_DATATYPE_BOOLEAN: for row in range(max_row_num): value = column_values[row] if not pd.isna(value): - tablet_add_value_by_index_bool(ctablet, row, col, value) + check_dataframe_tablet_error( + ctablet, + tablet_add_value_by_index_bool( + ctablet, row, col, value + ), + b"Failed to add BOOLEAN DataFrame value", + ) # INT32 elif data_type == TS_DATATYPE_INT32: for row in range(max_row_num): value = column_values[row] if not pd.isna(value): - tablet_add_value_by_index_int32_t(ctablet, row, col, value) + check_dataframe_tablet_error( + ctablet, + tablet_add_value_by_index_int32_t( + ctablet, row, col, value + ), + b"Failed to add INT32 DataFrame value", + ) # INT64 elif data_type == TS_DATATYPE_INT64 or data_type == TS_DATATYPE_TIMESTAMP: for row in range(max_row_num): value = column_values[row] if not pd.isna(value): - tablet_add_value_by_index_int64_t(ctablet, row, col, value) + check_dataframe_tablet_error( + ctablet, + tablet_add_value_by_index_int64_t( + ctablet, row, col, value + ), + b"Failed to add INT64 DataFrame value", + ) # FLOAT elif data_type == TS_DATATYPE_FLOAT: for row in range(max_row_num): value = column_values[row] if not pd.isna(value): - tablet_add_value_by_index_float(ctablet, row, col, value) + check_dataframe_tablet_error( + ctablet, + tablet_add_value_by_index_float( + ctablet, row, col, value + ), + b"Failed to add FLOAT DataFrame value", + ) # DOUBLE elif data_type == TS_DATATYPE_DOUBLE: for row in range(max_row_num): value = column_values[row] if not pd.isna(value): - tablet_add_value_by_index_double(ctablet, row, col, value) + check_dataframe_tablet_error( + ctablet, + tablet_add_value_by_index_double( + ctablet, row, col, value + ), + b"Failed to add DOUBLE DataFrame value", + ) # DATE (validated per-column above) elif data_type == TS_DATATYPE_DATE: for row in range(max_row_num): value = column_values[row] if not pd.isna(value): - tablet_add_value_by_index_int32_t(ctablet, row, col, parse_date_to_int(value)) + check_dataframe_tablet_error( + ctablet, + tablet_add_value_by_index_int32_t( + ctablet, row, col, parse_date_to_int(value) + ), + b"Failed to add DATE DataFrame value", + ) # STRING or TEXT (validated per-column above) elif data_type == TS_DATATYPE_STRING or data_type == TS_DATATYPE_TEXT: for row in range(max_row_num): @@ -463,7 +603,13 @@ cdef Tablet dataframe_to_c_tablet(object target_name, object dataframe, object t if not pd.isna(value): py_value = str(value) str_ptr = PyUnicode_AsUTF8AndSize(py_value, &raw_len) - tablet_add_value_by_index_string_with_len(ctablet, row, col, str_ptr, raw_len) + check_dataframe_tablet_error( + ctablet, + tablet_add_value_by_index_string_with_len( + ctablet, row, col, str_ptr, raw_len + ), + b"Failed to add STRING DataFrame value", + ) # BLOB (validated per-column above) elif data_type == TS_DATATYPE_BLOB: for row in range(max_row_num): @@ -471,11 +617,23 @@ cdef Tablet dataframe_to_c_tablet(object target_name, object dataframe, object t if not pd.isna(value): if isinstance(value, bytes): PyBytes_AsStringAndSize(value, &raw_str, &raw_len) - tablet_add_value_by_index_string_with_len(ctablet, row, col, raw_str, raw_len) + check_dataframe_tablet_error( + ctablet, + tablet_add_value_by_index_string_with_len( + ctablet, row, col, raw_str, raw_len + ), + b"Failed to add BLOB DataFrame value", + ) else: value_bytes = bytes(value) PyBytes_AsStringAndSize(value_bytes, &raw_str, &raw_len) - tablet_add_value_by_index_string_with_len(ctablet, row, col, raw_str, raw_len) + check_dataframe_tablet_error( + ctablet, + tablet_add_value_by_index_string_with_len( + ctablet, row, col, raw_str, raw_len + ), + b"Failed to add BLOB DataFrame value", + ) return ctablet @@ -485,42 +643,100 @@ cdef TsRecord to_c_record(object row_record): cdef bytes device_id_bytes = PyUnicode_AsUTF8String(row_record.get_device_id()) cdef const char * device_id = device_id_bytes cdef const char * str_ptr - cdef char * blob_ptr + cdef char * blob_ptr = NULL cdef Py_ssize_t str_len - cdef TsRecord record + cdef TsRecord record = NULL cdef int i cdef TSDataType data_type record = _ts_record_new(device_id, timestamp, field_num) - for i in range(field_num): - field = row_record.get_fields()[i] - data_type = to_c_data_type(field.get_data_type()) - if data_type == TS_DATATYPE_BOOLEAN: - _insert_data_into_ts_record_by_name_bool(record, PyUnicode_AsUTF8(field.get_field_name()), - field.get_bool_value()) - elif data_type == TS_DATATYPE_INT32 or data_type == TS_DATATYPE_DATE: - _insert_data_into_ts_record_by_name_int32_t(record, PyUnicode_AsUTF8(field.get_field_name()), - field.get_int_value()) - elif data_type == TS_DATATYPE_INT64: - _insert_data_into_ts_record_by_name_int64_t(record, PyUnicode_AsUTF8(field.get_field_name()), - field.get_long_value()) - elif data_type == TS_DATATYPE_TIMESTAMP: - _insert_data_into_ts_record_by_name_int64_t(record, PyUnicode_AsUTF8(field.get_field_name()), - field.get_timestamp_value()) - elif data_type == TS_DATATYPE_DOUBLE: - _insert_data_into_ts_record_by_name_double(record, PyUnicode_AsUTF8(field.get_field_name()), - field.get_double_value()) - elif data_type == TS_DATATYPE_FLOAT: - _insert_data_into_ts_record_by_name_float(record, PyUnicode_AsUTF8(field.get_field_name()), - field.get_float_value()) - elif data_type == TS_DATATYPE_TEXT or data_type == TS_DATATYPE_STRING: - str_ptr = PyUnicode_AsUTF8AndSize(field.get_string_value(), &str_len) - _insert_data_into_ts_record_by_name_string_with_len(record, PyUnicode_AsUTF8(field.get_field_name()), - str_ptr, str_len) - elif data_type == TS_DATATYPE_BLOB: - if PyBytes_AsStringAndSize(field.get_string_value(), &blob_ptr, &str_len) < 0: - raise ValueError("blob not legal") - _insert_data_into_ts_record_by_name_string_with_len(record, PyUnicode_AsUTF8(field.get_field_name()), - blob_ptr, str_len) + if record == NULL: + raise MemoryError("Failed to allocate native row record") + try: + for i in range(field_num): + field = row_record.get_fields()[i] + data_type = to_c_data_type(field.get_data_type()) + if data_type == TS_DATATYPE_BOOLEAN: + check_error( + _insert_data_into_ts_record_by_name_bool( + record, + PyUnicode_AsUTF8(field.get_field_name()), + field.get_bool_value(), + ), + b"Failed to add BOOLEAN row-record field", + ) + elif data_type == TS_DATATYPE_INT32 or data_type == TS_DATATYPE_DATE: + check_error( + _insert_data_into_ts_record_by_name_int32_t( + record, + PyUnicode_AsUTF8(field.get_field_name()), + field.get_int_value(), + ), + b"Failed to add INT32 row-record field", + ) + elif data_type == TS_DATATYPE_INT64: + check_error( + _insert_data_into_ts_record_by_name_int64_t( + record, + PyUnicode_AsUTF8(field.get_field_name()), + field.get_long_value(), + ), + b"Failed to add INT64 row-record field", + ) + elif data_type == TS_DATATYPE_TIMESTAMP: + check_error( + _insert_data_into_ts_record_by_name_int64_t( + record, + PyUnicode_AsUTF8(field.get_field_name()), + field.get_timestamp_value(), + ), + b"Failed to add TIMESTAMP row-record field", + ) + elif data_type == TS_DATATYPE_DOUBLE: + check_error( + _insert_data_into_ts_record_by_name_double( + record, + PyUnicode_AsUTF8(field.get_field_name()), + field.get_double_value(), + ), + b"Failed to add DOUBLE row-record field", + ) + elif data_type == TS_DATATYPE_FLOAT: + check_error( + _insert_data_into_ts_record_by_name_float( + record, + PyUnicode_AsUTF8(field.get_field_name()), + field.get_float_value(), + ), + b"Failed to add FLOAT row-record field", + ) + elif data_type == TS_DATATYPE_TEXT or data_type == TS_DATATYPE_STRING: + str_ptr = PyUnicode_AsUTF8AndSize(field.get_string_value(), &str_len) + check_error( + _insert_data_into_ts_record_by_name_string_with_len( + record, + PyUnicode_AsUTF8(field.get_field_name()), + str_ptr, + str_len, + ), + b"Failed to add STRING row-record field", + ) + elif data_type == TS_DATATYPE_BLOB: + if PyBytes_AsStringAndSize( + field.get_string_value(), &blob_ptr, &str_len + ) < 0: + raise TypeError("BLOB row-record values must be bytes") + check_error( + _insert_data_into_ts_record_by_name_string_with_len( + record, + PyUnicode_AsUTF8(field.get_field_name()), + blob_ptr, + str_len, + ), + b"Failed to add BLOB row-record field", + ) + except: + _free_tsfile_ts_record(&record) + raise return record # Free c structs' space @@ -549,7 +765,7 @@ cdef void free_c_row_record(TsRecord record): _free_tsfile_ts_record(&record) # Reader and writer new. -cdef TsFileWriter tsfile_writer_new_c(object pathname, uint64_t memory_threshold) except +: +cdef TsFileWriter tsfile_writer_new_c(object pathname, uint64_t memory_threshold) except NULL: cdef ErrorCode errno = 0 cdef TsFileWriter writer cdef bytes encoded_path = PyUnicode_AsUTF8String(pathname) @@ -558,7 +774,7 @@ cdef TsFileWriter tsfile_writer_new_c(object pathname, uint64_t memory_threshold check_error(errno) return writer -cdef TsFileReader tsfile_reader_new_c(object pathname) except +: +cdef TsFileReader tsfile_reader_new_c(object pathname) except NULL: cdef ErrorCode errno = 0 cdef TsFileReader reader cdef bytes encoded_path = PyUnicode_AsUTF8String(pathname) diff --git a/python/tsfile/tsfile_reader.pyx b/python/tsfile/tsfile_reader.pyx index f94d42053..4ca1ca714 100644 --- a/python/tsfile/tsfile_reader.pyx +++ b/python/tsfile/tsfile_reader.pyx @@ -164,9 +164,9 @@ cdef class ResultSetPy: code = tsfile_result_set_get_next_tsblock_as_arrow(self.result, &arrow_array, &arrow_schema) - if code == 21: # E_NO_MORE_DATA + if code == RET_NO_MORE_DATA: return None - if code != 0: + if code != RET_OK: check_error(code) if arrow_schema.release == NULL or arrow_array.release == NULL: diff --git a/python/tsfile/utils.py b/python/tsfile/utils.py index ea9263149..0ec54b3a7 100644 --- a/python/tsfile/utils.py +++ b/python/tsfile/utils.py @@ -142,7 +142,9 @@ def _gen(is_iterator: bool) -> Iterator[pd.DataFrame]: else: _table_name = _table_name.lower() if _table_name.lower() not in table_schema: - raise TableNotExistError(_table_name) + raise TableNotExistError( + context=f"Table '{_table_name}' does not exist" + ) table_schema = table_schema[_table_name] column_names_in_file = [] @@ -155,7 +157,9 @@ def _gen(is_iterator: bool) -> Iterator[pd.DataFrame]: if _column_names is not None: for column in _column_names: if column not in column_names_in_file and column != time_column: - raise ColumnNotExistError(column) + raise ColumnNotExistError( + context=f"Column '{column}' does not exist" + ) if ( table_schema.get_column(column).get_category() == ColumnCategory.FIELD From 4d4ec0d76f7f7e866a1a0961f516bb281e4dacac Mon Sep 17 00:00:00 2001 From: ColinLee Date: Mon, 20 Jul 2026 18:09:23 +0800 Subject: [PATCH 2/8] refactor: remove unused database error codes --- cpp/examples/c_examples/demo_read.c | 2 +- cpp/src/cwrapper/errno_define_c.h | 24 ------------------- cpp/src/reader/tsfile_executor.cc | 2 +- cpp/src/utils/errno_define.h | 17 ------------- cpp/tools/format/output_format.cc | 4 ---- python/tests/test_exceptions.py | 18 ++++++++++++++ python/tsfile/exceptions.py | 37 ----------------------------- 7 files changed, 20 insertions(+), 84 deletions(-) diff --git a/cpp/examples/c_examples/demo_read.c b/cpp/examples/c_examples/demo_read.c index 5ac6111e7..efa6b63d0 100644 --- a/cpp/examples/c_examples/demo_read.c +++ b/cpp/examples/c_examples/demo_read.c @@ -37,7 +37,7 @@ ERRNO read_tsfile() { HANDLE_ERROR(code); if (ret == NULL) { - HANDLE_ERROR(RET_INVALID_QUERY); + HANDLE_ERROR(RET_INVALID_ARG); } // Get query result metadata: column name and datatype diff --git a/cpp/src/cwrapper/errno_define_c.h b/cpp/src/cwrapper/errno_define_c.h index af9cd1ca1..fc62a51f5 100644 --- a/cpp/src/cwrapper/errno_define_c.h +++ b/cpp/src/cwrapper/errno_define_c.h @@ -28,23 +28,10 @@ #define RET_OUT_OF_RANGE 5 #define RET_PARTIAL_READ 6 #define RET_INVALID_SCHEMA 8 -#define RET_NET_EPOLL_ERR 9 -#define RET_NET_EPOLL_WAIT_ERR 10 -#define RET_NET_RECV_ERR 11 -#define RET_NET_ACCEPT_ERR 12 -#define RET_NET_FCNTL_ERR 13 -#define RET_NET_LISTEN_ERR 14 -#define RET_NET_SEND_ERR 15 -#define RET_PIPE_ERR 16 -#define RET_THREAD_CREATE_ERR 17 -#define RET_MUTEX_ERR 18 -#define RET_COND_ERR 19 #define RET_OVERFLOW 20 #define RET_NO_MORE_DATA 21 #define RET_OUT_OF_ORDER 22 -#define RET_TSBLOCK_TYPE_NOT_SUPPORTED 23 #define RET_TSBLOCK_DATA_INCONSISTENCY 24 -#define RET_DDL_UNKNOWN_TYPE 25 #define RET_TYPE_NOT_SUPPORTED 26 #define RET_TYPE_NOT_MATCH 27 #define RET_FILE_OPEN_ERR 28 @@ -56,17 +43,11 @@ #define RET_FILE_STAT_ERR 34 #define RET_TSFILE_CORRUPTED 35 #define RET_BUF_NOT_ENOUGH 36 -#define RET_INVALID_PATH 37 #define RET_NOT_MATCH 38 -#define RET_JSON_INVALID 39 #define RET_NOT_SUPPORT 40 -#define RET_PARSER_ERR 41 -#define RET_ANALYZE_ERR 42 #define RET_INVALID_DATA_POINT 43 #define RET_DEVICE_NOT_EXIST 44 #define RET_MEASUREMENT_NOT_EXIST 45 -#define RET_INVALID_QUERY 46 -#define RET_SDK_QUERY_OPTIMIZE_ERR 47 #define RET_COMPRESS_ERR 48 #define RET_TABLE_NOT_EXIST 49 #define RET_COLUMN_NOT_EXIST 50 @@ -76,10 +57,7 @@ #define RET_DECODE_ERR 54 /* Backward-compatible aliases for identifiers published with misspellings. */ -#define RET_PIPRET_ERR RET_PIPE_ERR -#define RET_THREAD_CREATRET_ERR RET_THREAD_CREATE_ERR #define RET_NO_MORRET_DATA RET_NO_MORE_DATA -#define RET_TSBLOCK_TYPRET_NOT_SUPPORTED RET_TSBLOCK_TYPE_NOT_SUPPORTED #define RET_TYPRET_NOT_SUPPORTED RET_TYPE_NOT_SUPPORTED #define RET_TYPRET_NOT_MATCH RET_TYPE_NOT_MATCH #define RET_FILRET_OPEN_ERR RET_FILE_OPEN_ERR @@ -90,9 +68,7 @@ #define RET_TSFILRET_WRITER_META_ERR RET_TSFILE_WRITER_META_ERR #define RET_FILRET_STAT_ERR RET_FILE_STAT_ERR #define RET_TSFILRET_CORRUPTED RET_TSFILE_CORRUPTED -#define RET_ANALYZRET_ERR RET_ANALYZE_ERR #define RET_DEVICRET_NOT_EXIST RET_DEVICE_NOT_EXIST -#define RET_SDK_QUERY_OPTIMIZRET_ERR RET_SDK_QUERY_OPTIMIZE_ERR #define RET_TABLRET_NOT_EXIST RET_TABLE_NOT_EXIST #define RET_INVALID_NODRET_TYPE RET_INVALID_NODE_TYPE diff --git a/cpp/src/reader/tsfile_executor.cc b/cpp/src/reader/tsfile_executor.cc index f2d34167d..15aaa161b 100644 --- a/cpp/src/reader/tsfile_executor.cc +++ b/cpp/src/reader/tsfile_executor.cc @@ -67,7 +67,7 @@ int TsFileExecutor::execute(QueryExpression* query_expr, ResultSet*& ret_qds) { if (query_exprs_->has_filter_) { regular_expr = query_exprs_->optimize(origin_expr, paths); if (regular_expr == nullptr) { - return E_SDK_QUERY_OPTIMIZE_ERR; + return E_INVALID_ARG; } query_exprs_->set_expression(regular_expr); } diff --git a/cpp/src/utils/errno_define.h b/cpp/src/utils/errno_define.h index f52cead99..2f75ce9fa 100644 --- a/cpp/src/utils/errno_define.h +++ b/cpp/src/utils/errno_define.h @@ -29,17 +29,6 @@ const int E_INVALID_ARG = 4; const int E_OUT_OF_RANGE = 5; const int E_PARTIAL_READ = 6; const int E_INVALID_SCHEMA = 8; -const int E_NET_EPOLL_ERR = 9; -const int E_NET_EPOLL_WAIT_ERR = 10; -const int E_NET_RECV_ERR = 11; -const int E_NET_ACCEPT_ERR = 12; -const int E_NET_FCNTL_ERR = 13; -const int E_NET_LISTEN_ERR = 14; -const int E_NET_SEND_ERR = 15; -const int E_PIPE_ERR = 16; -const int E_THREAD_CREATE_ERR = 17; -const int E_MUTEX_ERR = 18; -const int E_COND_ERR = 19; const int E_OVERFLOW = 20; const int E_NO_MORE_DATA = 21; const int E_OUT_OF_ORDER = 22; @@ -55,17 +44,11 @@ const int E_TSFILE_WRITER_META_ERR = 33; const int E_FILE_STAT_ERR = 34; const int E_TSFILE_CORRUPTED = 35; const int E_BUF_NOT_ENOUGH = 36; -const int E_INVALID_PATH = 37; const int E_NOT_MATCH = 38; -const int E_JSON_INVALID = 39; const int E_NOT_SUPPORT = 40; -const int E_PARSER_ERR = 41; -const int E_ANALYZE_ERR = 42; const int E_INVALID_DATA_POINT = 43; const int E_DEVICE_NOT_EXIST = 44; const int E_MEASUREMENT_NOT_EXIST = 45; -const int E_INVALID_QUERY = 46; -const int E_SDK_QUERY_OPTIMIZE_ERR = 47; const int E_COMPRESS_ERR = 48; const int E_TABLE_NOT_EXIST = 49; const int E_COLUMN_NOT_EXIST = 50; diff --git a/cpp/tools/format/output_format.cc b/cpp/tools/format/output_format.cc index e9ae8cb0b..3c095a352 100644 --- a/cpp/tools/format/output_format.cc +++ b/cpp/tools/format/output_format.cc @@ -48,8 +48,6 @@ const char* error_code_message(int code) { return "file read error"; case common::E_TSFILE_CORRUPTED: return "file is corrupted"; - case common::E_INVALID_PATH: - return "invalid path"; case common::E_DEVICE_NOT_EXIST: return "device does not exist"; case common::E_MEASUREMENT_NOT_EXIST: @@ -58,8 +56,6 @@ const char* error_code_message(int code) { return "table does not exist"; case common::E_COLUMN_NOT_EXIST: return "column does not exist"; - case common::E_INVALID_QUERY: - return "invalid query"; case common::E_TYPE_NOT_SUPPORTED: return "data type not supported"; case common::E_TYPE_NOT_MATCH: diff --git a/python/tests/test_exceptions.py b/python/tests/test_exceptions.py index 7d025dc27..babf1b947 100644 --- a/python/tests/test_exceptions.py +++ b/python/tests/test_exceptions.py @@ -41,6 +41,24 @@ def test_get_exception_preserves_known_and_unknown_codes(): assert unknown.code == 999 assert unknown.message == "opening reader: Unknown library error" + retired_codes = { + *range(9, 20), + 23, + 25, + 37, + 39, + 41, + 42, + 46, + 47, + } + assert retired_codes.isdisjoint(error.value for error in ErrorCode) + for code in retired_codes: + retired = get_exception(code) + assert type(retired) is LibraryError + assert retired.code == code + assert retired.message == "Unknown library error" + def test_writer_constructor_propagates_native_error(tmp_path, capsys): path = tmp_path / "already-exists.tsfile" diff --git a/python/tsfile/exceptions.py b/python/tsfile/exceptions.py index 8f355c21c..ff1d11850 100644 --- a/python/tsfile/exceptions.py +++ b/python/tsfile/exceptions.py @@ -29,23 +29,10 @@ class ErrorCode(IntEnum): OUT_OF_RANGE = 5 PARTIAL_READ = 6 INVALID_SCHEMA = 8 - NET_EPOLL_ERROR = 9 - NET_EPOLL_WAIT_ERROR = 10 - NET_RECEIVE_ERROR = 11 - NET_ACCEPT_ERROR = 12 - NET_FCNTL_ERROR = 13 - NET_LISTEN_ERROR = 14 - NET_SEND_ERROR = 15 - PIPE_ERROR = 16 - THREAD_CREATE_ERROR = 17 - MUTEX_ERROR = 18 - CONDITION_ERROR = 19 OVERFLOW = 20 NO_MORE_DATA = 21 OUT_OF_ORDER = 22 - TSBLOCK_TYPE_NOT_SUPPORTED = 23 DATA_INCONSISTENCY = 24 - DDL_UNKNOWN_TYPE = 25 TYPE_NOT_SUPPORTED = 26 TYPE_MISMATCH = 27 FILE_OPEN_ERROR = 28 @@ -57,17 +44,11 @@ class ErrorCode(IntEnum): FILE_STAT_ERROR = 34 TSFILE_CORRUPTED = 35 BUFFER_NOT_ENOUGH = 36 - INVALID_PATH = 37 NOT_MATCH = 38 - JSON_INVALID = 39 NOT_SUPPORTED = 40 - PARSER_ERROR = 41 - ANALYZE_ERROR = 42 INVALID_DATA_POINT = 43 DEVICE_NOT_EXIST = 44 MEASUREMENT_NOT_EXIST = 45 - INVALID_QUERY = 46 - QUERY_OPTIMIZE_ERROR = 47 COMPRESSION_ERROR = 48 TABLE_NOT_EXIST = 49 COLUMN_NOT_EXIST = 50 @@ -190,11 +171,6 @@ class BufferNotEnoughError(LibraryError): _default_code = 36 -class InvalidPathError(LibraryError): - _default_message = "Invalid path" - _default_code = 37 - - class NotSupportedError(LibraryError): _default_message = "Not support yet" _default_code = 40 @@ -210,16 +186,6 @@ class MeasurementNotExistError(LibraryError): _default_code = 45 -class InvalidQueryError(LibraryError): - _default_message = "Malformed query syntax" - _default_code = 46 - - -class QueryOptimizeError(LibraryError): - _default_message = "Failed to optimize query" - _default_code = 47 - - class CompressionError(LibraryError): _default_message = "Data compression/decompression failed" _default_code = 48 @@ -288,12 +254,9 @@ class DecodeError(LibraryError): 34: FileStatError, 35: TsFileCorruptedError, 36: BufferNotEnoughError, - 37: InvalidPathError, 40: NotSupportedError, 44: DeviceNotExistError, 45: MeasurementNotExistError, - 46: InvalidQueryError, - 47: QueryOptimizeError, 48: CompressionError, 49: TableNotExistError, 50: ColumnNotExistError, From 66e95ffb358aec88e0cf226d57077adef3d3e8fd Mon Sep 17 00:00:00 2001 From: ColinLee Date: Mon, 20 Jul 2026 18:33:05 +0800 Subject: [PATCH 3/8] fix: preserve error code compatibility --- cpp/src/cwrapper/errno_define_c.h | 29 +++++++++++++++++ cpp/src/reader/tsfile_reader.cc | 32 ++++++++++++++----- cpp/src/utils/errno_define.h | 20 ++++++++++++ cpp/test/cwrapper/c_release_test.cc | 30 +++++++++++++++++ .../cwrapper/query_by_row_cwrapper_test.cc | 27 ++++++++++++++++ cpp/tools/format/output_format.cc | 2 ++ python/tests/test_exceptions.py | 28 ++++++++++++++-- python/tsfile/exceptions.py | 15 +++++++++ 8 files changed, 173 insertions(+), 10 deletions(-) diff --git a/cpp/src/cwrapper/errno_define_c.h b/cpp/src/cwrapper/errno_define_c.h index fc62a51f5..de55b8f3b 100644 --- a/cpp/src/cwrapper/errno_define_c.h +++ b/cpp/src/cwrapper/errno_define_c.h @@ -43,6 +43,7 @@ #define RET_FILE_STAT_ERR 34 #define RET_TSFILE_CORRUPTED 35 #define RET_BUF_NOT_ENOUGH 36 +#define RET_INVALID_PATH 37 #define RET_NOT_MATCH 38 #define RET_NOT_SUPPORT 40 #define RET_INVALID_DATA_POINT 43 @@ -56,8 +57,34 @@ #define RET_ENCODE_ERR 53 #define RET_DECODE_ERR 54 +/* + * Deprecated legacy values retained for source compatibility. The current + * TsFile implementation does not return these codes. + */ +#define RET_NET_EPOLL_ERR 9 +#define RET_NET_EPOLL_WAIT_ERR 10 +#define RET_NET_RECV_ERR 11 +#define RET_NET_ACCEPT_ERR 12 +#define RET_NET_FCNTL_ERR 13 +#define RET_NET_LISTEN_ERR 14 +#define RET_NET_SEND_ERR 15 +#define RET_PIPE_ERR 16 +#define RET_THREAD_CREATE_ERR 17 +#define RET_MUTEX_ERR 18 +#define RET_COND_ERR 19 +#define RET_TSBLOCK_TYPE_NOT_SUPPORTED 23 +#define RET_DDL_UNKNOWN_TYPE 25 +#define RET_JSON_INVALID 39 +#define RET_PARSER_ERR 41 +#define RET_ANALYZE_ERR 42 +#define RET_INVALID_QUERY 46 +#define RET_SDK_QUERY_OPTIMIZE_ERR 47 + /* Backward-compatible aliases for identifiers published with misspellings. */ +#define RET_PIPRET_ERR RET_PIPE_ERR +#define RET_THREAD_CREATRET_ERR RET_THREAD_CREATE_ERR #define RET_NO_MORRET_DATA RET_NO_MORE_DATA +#define RET_TSBLOCK_TYPRET_NOT_SUPPORTED RET_TSBLOCK_TYPE_NOT_SUPPORTED #define RET_TYPRET_NOT_SUPPORTED RET_TYPE_NOT_SUPPORTED #define RET_TYPRET_NOT_MATCH RET_TYPE_NOT_MATCH #define RET_FILRET_OPEN_ERR RET_FILE_OPEN_ERR @@ -68,7 +95,9 @@ #define RET_TSFILRET_WRITER_META_ERR RET_TSFILE_WRITER_META_ERR #define RET_FILRET_STAT_ERR RET_FILE_STAT_ERR #define RET_TSFILRET_CORRUPTED RET_TSFILE_CORRUPTED +#define RET_ANALYZRET_ERR RET_ANALYZE_ERR #define RET_DEVICRET_NOT_EXIST RET_DEVICE_NOT_EXIST +#define RET_SDK_QUERY_OPTIMIZRET_ERR RET_SDK_QUERY_OPTIMIZE_ERR #define RET_TABLRET_NOT_EXIST RET_TABLE_NOT_EXIST #define RET_INVALID_NODRET_TYPE RET_INVALID_NODE_TYPE diff --git a/cpp/src/reader/tsfile_reader.cc b/cpp/src/reader/tsfile_reader.cc index 540674f33..33d0d8967 100644 --- a/cpp/src/reader/tsfile_reader.cc +++ b/cpp/src/reader/tsfile_reader.cc @@ -18,6 +18,8 @@ */ #include "tsfile_reader.h" +#include + #include "common/schema.h" #include "filter/time_operator.h" #include "tsfile_executor.h" @@ -33,6 +35,19 @@ struct DeviceMetaEntry { int64_t end_offset; }; +int parse_paths(const std::vector& path_list, + std::vector& parsed_paths) { + try { + parsed_paths.reserve(path_list.size()); + for (const auto& path : path_list) { + parsed_paths.emplace_back(path, true); + } + } catch (const std::runtime_error&) { + return E_INVALID_PATH; + } + return E_OK; +} + int get_all_device_entries(std::vector& entries, std::shared_ptr index_node, ReadFile* read_file, PageArena& pa) { @@ -152,14 +167,15 @@ int TsFileReader::query(QueryExpression* qe, ResultSet*& ret_qds) { int TsFileReader::query(std::vector& path_list, int64_t start_time, int64_t end_time, ResultSet*& result_set) { - int ret = E_OK; + std::vector path_list_vec; + int ret = parse_paths(path_list, path_list_vec); + if (ret != E_OK) { + return ret; + } + Filter* time_filter = new TimeBetween(start_time, end_time, false); Expression* exp = new storage::Expression(storage::GLOBALTIME_EXPR, time_filter); - std::vector path_list_vec; - for (const auto& path : path_list) { - path_list_vec.emplace_back(Path(path, true)); - } QueryExpression* query_expression = QueryExpression::create(path_list_vec, exp); ret = tsfile_executor_->execute(query_expression, result_set); @@ -200,10 +216,10 @@ int TsFileReader::query(const std::string& table_name, int TsFileReader::queryByRow(std::vector& path_list, int offset, int limit, ResultSet*& result_set) { - int ret = E_OK; std::vector path_list_vec; - for (const auto& path : path_list) { - path_list_vec.emplace_back(Path(path, true)); + int ret = parse_paths(path_list, path_list_vec); + if (ret != E_OK) { + return ret; } QueryExpression* query_expression = QueryExpression::create(path_list_vec, nullptr); diff --git a/cpp/src/utils/errno_define.h b/cpp/src/utils/errno_define.h index 2f75ce9fa..840f0d49a 100644 --- a/cpp/src/utils/errno_define.h +++ b/cpp/src/utils/errno_define.h @@ -44,6 +44,7 @@ const int E_TSFILE_WRITER_META_ERR = 33; const int E_FILE_STAT_ERR = 34; const int E_TSFILE_CORRUPTED = 35; const int E_BUF_NOT_ENOUGH = 36; +const int E_INVALID_PATH = 37; const int E_NOT_MATCH = 38; const int E_NOT_SUPPORT = 40; const int E_INVALID_DATA_POINT = 43; @@ -57,6 +58,25 @@ const int E_INVALID_NODE_TYPE = 52; const int E_ENCODE_ERR = 53; const int E_DECODE_ERR = 54; +// Deprecated legacy values retained for source compatibility. The current +// TsFile implementation does not return these codes. +const int E_NET_EPOLL_ERR = 9; +const int E_NET_EPOLL_WAIT_ERR = 10; +const int E_NET_RECV_ERR = 11; +const int E_NET_ACCEPT_ERR = 12; +const int E_NET_FCNTL_ERR = 13; +const int E_NET_LISTEN_ERR = 14; +const int E_NET_SEND_ERR = 15; +const int E_PIPE_ERR = 16; +const int E_THREAD_CREATE_ERR = 17; +const int E_MUTEX_ERR = 18; +const int E_COND_ERR = 19; +const int E_JSON_INVALID = 39; +const int E_PARSER_ERR = 41; +const int E_ANALYZE_ERR = 42; +const int E_INVALID_QUERY = 46; +const int E_SDK_QUERY_OPTIMIZE_ERR = 47; + } // end namespace common #endif // UTILS_ERRNO_DEFINE_H diff --git a/cpp/test/cwrapper/c_release_test.cc b/cpp/test/cwrapper/c_release_test.cc index f27e049e0..11885bcb0 100644 --- a/cpp/test/cwrapper/c_release_test.cc +++ b/cpp/test/cwrapper/c_release_test.cc @@ -38,6 +38,36 @@ extern "C" { namespace CReleaseTest { class CReleaseTest : public testing::Test {}; +TEST_F(CReleaseTest, PublishedErrorCodesRemainSourceCompatible) { + const int published_codes[] = { + RET_NET_EPOLL_ERR, + RET_NET_EPOLL_WAIT_ERR, + RET_NET_RECV_ERR, + RET_NET_ACCEPT_ERR, + RET_NET_FCNTL_ERR, + RET_NET_LISTEN_ERR, + RET_NET_SEND_ERR, + RET_PIPRET_ERR, + RET_THREAD_CREATRET_ERR, + RET_MUTEX_ERR, + RET_COND_ERR, + RET_TSBLOCK_TYPRET_NOT_SUPPORTED, + RET_DDL_UNKNOWN_TYPE, + RET_INVALID_PATH, + RET_JSON_INVALID, + RET_PARSER_ERR, + RET_ANALYZRET_ERR, + RET_INVALID_QUERY, + RET_SDK_QUERY_OPTIMIZRET_ERR, + }; + const int expected_codes[] = {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 23, 25, 37, 39, 41, 42, 46, 47}; + + for (int i = 0; i < 19; ++i) { + EXPECT_EQ(published_codes[i], expected_codes[i]); + } +} + TEST_F(CReleaseTest, TestCreateFile) { ERRNO error_no = RET_OK; remove("create_file1.tsfile"); diff --git a/cpp/test/cwrapper/query_by_row_cwrapper_test.cc b/cpp/test/cwrapper/query_by_row_cwrapper_test.cc index 4983c57ea..6f36c491c 100644 --- a/cpp/test/cwrapper/query_by_row_cwrapper_test.cc +++ b/cpp/test/cwrapper/query_by_row_cwrapper_test.cc @@ -193,6 +193,33 @@ TEST_F(CWrapperQueryByRowTest, TreeByRowOffsetLimit) { storage::libtsfile_destroy(); } +TEST_F(CWrapperQueryByRowTest, InvalidTreePathReturnsErrorCode) { + storage::libtsfile_init(); + + const char* file_name = "cwrapper_invalid_tree_path_test.tsfile"; + remove(file_name); + write_tree_tsfile(file_name, {"root.d1"}, {"s1"}, 1); + + ERRNO code = RET_OK; + TsFileReader reader = tsfile_reader_new(file_name, &code); + ASSERT_EQ(code, RET_OK); + ASSERT_NE(reader, nullptr); + + char device_id[] = "root.d1"; + char invalid_measurement[] = "a*%"; + char* device_ids[] = {device_id}; + char* measurement_ids[] = {invalid_measurement}; + ResultSet result = tsfile_reader_query_tree_by_row( + reader, device_ids, 1, measurement_ids, 1, 0, -1, &code); + + EXPECT_EQ(code, RET_INVALID_PATH); + EXPECT_EQ(result, nullptr); + EXPECT_EQ(tsfile_reader_close(reader), RET_OK); + remove(file_name); + + storage::libtsfile_destroy(); +} + TEST_F(CWrapperQueryByRowTest, TableByRowOffsetLimit) { storage::libtsfile_init(); diff --git a/cpp/tools/format/output_format.cc b/cpp/tools/format/output_format.cc index 3c095a352..2962cda25 100644 --- a/cpp/tools/format/output_format.cc +++ b/cpp/tools/format/output_format.cc @@ -48,6 +48,8 @@ const char* error_code_message(int code) { return "file read error"; case common::E_TSFILE_CORRUPTED: return "file is corrupted"; + case common::E_INVALID_PATH: + return "invalid path"; case common::E_DEVICE_NOT_EXIST: return "device does not exist"; case common::E_MEASUREMENT_NOT_EXIST: diff --git a/python/tests/test_exceptions.py b/python/tests/test_exceptions.py index babf1b947..0466af58d 100644 --- a/python/tests/test_exceptions.py +++ b/python/tests/test_exceptions.py @@ -24,6 +24,8 @@ ErrorCode, FileOpenError, InvalidArgumentError, + InvalidPathError, + InvalidQueryError, LibraryError, TsFileCorruptedError, get_exception, @@ -45,11 +47,9 @@ def test_get_exception_preserves_known_and_unknown_codes(): *range(9, 20), 23, 25, - 37, 39, 41, 42, - 46, 47, } assert retired_codes.isdisjoint(error.value for error in ErrorCode) @@ -59,6 +59,30 @@ def test_get_exception_preserves_known_and_unknown_codes(): assert retired.code == code assert retired.message == "Unknown library error" + invalid_path = get_exception(37) + assert isinstance(invalid_path, InvalidPathError) + assert invalid_path.code == ErrorCode.INVALID_PATH + + # InvalidQueryError was public before ErrorCode was introduced. Keep the + # mapping for compatibility without advertising 46 as an active code. + invalid_query = get_exception(46) + assert isinstance(invalid_query, InvalidQueryError) + assert invalid_query.code == 46 + + +def test_invalid_tree_path_propagates_native_error(tmp_path): + path = tmp_path / "invalid-path.tsfile" + with TsFileWriter(str(path)) as writer: + writer.register_timeseries( + "root.device", TimeseriesSchema("value", TSDataType.INT64) + ) + + with TsFileReader(str(path)) as reader: + with pytest.raises(InvalidPathError) as exc_info: + reader.query_timeseries("root.device", ["a*%"], 0, 2) + + assert exc_info.value.code == ErrorCode.INVALID_PATH + def test_writer_constructor_propagates_native_error(tmp_path, capsys): path = tmp_path / "already-exists.tsfile" diff --git a/python/tsfile/exceptions.py b/python/tsfile/exceptions.py index ff1d11850..cf49cf3df 100644 --- a/python/tsfile/exceptions.py +++ b/python/tsfile/exceptions.py @@ -44,6 +44,7 @@ class ErrorCode(IntEnum): FILE_STAT_ERROR = 34 TSFILE_CORRUPTED = 35 BUFFER_NOT_ENOUGH = 36 + INVALID_PATH = 37 NOT_MATCH = 38 NOT_SUPPORTED = 40 INVALID_DATA_POINT = 43 @@ -171,6 +172,11 @@ class BufferNotEnoughError(LibraryError): _default_code = 36 +class InvalidPathError(LibraryError): + _default_message = "Invalid path" + _default_code = 37 + + class NotSupportedError(LibraryError): _default_message = "Not support yet" _default_code = 40 @@ -186,6 +192,13 @@ class MeasurementNotExistError(LibraryError): _default_code = 45 +class InvalidQueryError(LibraryError): + """Compatibility exception for the legacy query error code.""" + + _default_message = "Malformed query syntax" + _default_code = 46 + + class CompressionError(LibraryError): _default_message = "Data compression/decompression failed" _default_code = 48 @@ -254,9 +267,11 @@ class DecodeError(LibraryError): 34: FileStatError, 35: TsFileCorruptedError, 36: BufferNotEnoughError, + 37: InvalidPathError, 40: NotSupportedError, 44: DeviceNotExistError, 45: MeasurementNotExistError, + 46: InvalidQueryError, 48: CompressionError, 49: TableNotExistError, 50: ColumnNotExistError, From e05f703e38f5fc17daab296f24e3a0df1b4180bb Mon Sep 17 00:00:00 2001 From: ColinLee Date: Wed, 22 Jul 2026 11:21:57 +0800 Subject: [PATCH 4/8] Fix Python error handling lifecycle --- cpp/src/cwrapper/errno_define_c.h | 1 - cpp/src/utils/errno_define.h | 1 - cpp/test/cwrapper/c_release_test.cc | 7 +++---- python/tests/test_exceptions.py | 8 +------- python/tsfile/exceptions.py | 8 -------- python/tsfile/tsfile_reader.pyx | 14 +++++++++++--- python/tsfile/tsfile_writer.pyx | 6 +++++- 7 files changed, 20 insertions(+), 25 deletions(-) diff --git a/cpp/src/cwrapper/errno_define_c.h b/cpp/src/cwrapper/errno_define_c.h index de55b8f3b..fd167df58 100644 --- a/cpp/src/cwrapper/errno_define_c.h +++ b/cpp/src/cwrapper/errno_define_c.h @@ -77,7 +77,6 @@ #define RET_JSON_INVALID 39 #define RET_PARSER_ERR 41 #define RET_ANALYZE_ERR 42 -#define RET_INVALID_QUERY 46 #define RET_SDK_QUERY_OPTIMIZE_ERR 47 /* Backward-compatible aliases for identifiers published with misspellings. */ diff --git a/cpp/src/utils/errno_define.h b/cpp/src/utils/errno_define.h index 840f0d49a..8bb01235f 100644 --- a/cpp/src/utils/errno_define.h +++ b/cpp/src/utils/errno_define.h @@ -74,7 +74,6 @@ const int E_COND_ERR = 19; const int E_JSON_INVALID = 39; const int E_PARSER_ERR = 41; const int E_ANALYZE_ERR = 42; -const int E_INVALID_QUERY = 46; const int E_SDK_QUERY_OPTIMIZE_ERR = 47; } // end namespace common diff --git a/cpp/test/cwrapper/c_release_test.cc b/cpp/test/cwrapper/c_release_test.cc index 11885bcb0..10e87f3f6 100644 --- a/cpp/test/cwrapper/c_release_test.cc +++ b/cpp/test/cwrapper/c_release_test.cc @@ -57,13 +57,12 @@ TEST_F(CReleaseTest, PublishedErrorCodesRemainSourceCompatible) { RET_JSON_INVALID, RET_PARSER_ERR, RET_ANALYZRET_ERR, - RET_INVALID_QUERY, RET_SDK_QUERY_OPTIMIZRET_ERR, }; - const int expected_codes[] = {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, - 19, 23, 25, 37, 39, 41, 42, 46, 47}; + const int expected_codes[] = {9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 23, 25, 37, 39, 41, 42, 47}; - for (int i = 0; i < 19; ++i) { + for (int i = 0; i < 18; ++i) { EXPECT_EQ(published_codes[i], expected_codes[i]); } } diff --git a/python/tests/test_exceptions.py b/python/tests/test_exceptions.py index 0466af58d..dbc12e792 100644 --- a/python/tests/test_exceptions.py +++ b/python/tests/test_exceptions.py @@ -25,7 +25,6 @@ FileOpenError, InvalidArgumentError, InvalidPathError, - InvalidQueryError, LibraryError, TsFileCorruptedError, get_exception, @@ -50,6 +49,7 @@ def test_get_exception_preserves_known_and_unknown_codes(): 39, 41, 42, + 46, 47, } assert retired_codes.isdisjoint(error.value for error in ErrorCode) @@ -63,12 +63,6 @@ def test_get_exception_preserves_known_and_unknown_codes(): assert isinstance(invalid_path, InvalidPathError) assert invalid_path.code == ErrorCode.INVALID_PATH - # InvalidQueryError was public before ErrorCode was introduced. Keep the - # mapping for compatibility without advertising 46 as an active code. - invalid_query = get_exception(46) - assert isinstance(invalid_query, InvalidQueryError) - assert invalid_query.code == 46 - def test_invalid_tree_path_propagates_native_error(tmp_path): path = tmp_path / "invalid-path.tsfile" diff --git a/python/tsfile/exceptions.py b/python/tsfile/exceptions.py index cf49cf3df..c0a963862 100644 --- a/python/tsfile/exceptions.py +++ b/python/tsfile/exceptions.py @@ -192,13 +192,6 @@ class MeasurementNotExistError(LibraryError): _default_code = 45 -class InvalidQueryError(LibraryError): - """Compatibility exception for the legacy query error code.""" - - _default_message = "Malformed query syntax" - _default_code = 46 - - class CompressionError(LibraryError): _default_message = "Data compression/decompression failed" _default_code = 48 @@ -271,7 +264,6 @@ class DecodeError(LibraryError): 40: NotSupportedError, 44: DeviceNotExistError, 45: MeasurementNotExistError, - 46: InvalidQueryError, 48: CompressionError, 49: TableNotExistError, 50: ColumnNotExistError, diff --git a/python/tsfile/tsfile_reader.pyx b/python/tsfile/tsfile_reader.pyx index 4ca1ca714..36374adde 100644 --- a/python/tsfile/tsfile_reader.pyx +++ b/python/tsfile/tsfile_reader.pyx @@ -58,6 +58,7 @@ cdef class ResultSetPy: cdef object is_tree def __init__(self, tsfile_reader : TsFileReaderPy, is_tree: bint = False): + self.result = NULL self.metadata = None self.valid = True self.tsfile_reader = weakref.ref(tsfile_reader) @@ -296,7 +297,10 @@ cdef class ResultSetPy: self.close() def __dealloc__(self): - self.close() + try: + self.close() + except Exception: + pass def __enter__(self): return self @@ -321,8 +325,9 @@ cdef class TsFileReaderPy: """ Initialize a TsFile reader for the specified file path. """ - self.init_reader(pathname) + self.reader = NULL self.activate_result_set_list = weakref.WeakSet() + self.init_reader(pathname) cdef init_reader(self, pathname): self.reader = tsfile_reader_new_c(pathname) @@ -533,7 +538,10 @@ cdef class TsFileReaderPy: return self.activate_result_set_list def __dealloc__(self): - self.close() + try: + self.close() + except Exception: + pass def __enter__(self): return self diff --git a/python/tsfile/tsfile_writer.pyx b/python/tsfile/tsfile_writer.pyx index 30a29d3bb..9e84d83c0 100644 --- a/python/tsfile/tsfile_writer.pyx +++ b/python/tsfile/tsfile_writer.pyx @@ -33,6 +33,7 @@ cdef class TsFileWriterPy: cdef TsFileWriter writer def __init__(self, pathname: str, memory_threshold: int = 128 * 1024 * 1024): + self.writer = NULL self.writer = tsfile_writer_new_c(pathname, memory_threshold) def register_timeseries(self, device_name : str, timeseries_schema : TimeseriesSchemaPy): @@ -185,7 +186,10 @@ cdef class TsFileWriterPy: check_error(errno) def __dealloc__(self): - self.close() + try: + self.close() + except Exception: + pass def __enter__(self): return self From da5efde606584020a4a942d6a32d1c9cf91bc513 Mon Sep 17 00:00:00 2001 From: ColinLee Date: Wed, 22 Jul 2026 16:38:10 +0800 Subject: [PATCH 5/8] Initialize Cython native handles defensively --- python/tsfile/tsfile_py_cpp.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/tsfile/tsfile_py_cpp.pyx b/python/tsfile/tsfile_py_cpp.pyx index e36b548ac..936c74af1 100644 --- a/python/tsfile/tsfile_py_cpp.pyx +++ b/python/tsfile/tsfile_py_cpp.pyx @@ -767,7 +767,7 @@ cdef void free_c_row_record(TsRecord record): # Reader and writer new. cdef TsFileWriter tsfile_writer_new_c(object pathname, uint64_t memory_threshold) except NULL: cdef ErrorCode errno = 0 - cdef TsFileWriter writer + cdef TsFileWriter writer = NULL cdef bytes encoded_path = PyUnicode_AsUTF8String(pathname) cdef const char * c_path = encoded_path writer = _tsfile_writer_new(c_path, memory_threshold, &errno) @@ -776,7 +776,7 @@ cdef TsFileWriter tsfile_writer_new_c(object pathname, uint64_t memory_threshold cdef TsFileReader tsfile_reader_new_c(object pathname) except NULL: cdef ErrorCode errno = 0 - cdef TsFileReader reader + cdef TsFileReader reader = NULL cdef bytes encoded_path = PyUnicode_AsUTF8String(pathname) cdef const char * c_path = encoded_path reader = tsfile_reader_new(c_path, &errno) From e95bc00ca35a70eb9232d874fd2512b814dc854f Mon Sep 17 00:00:00 2001 From: ColinLee Date: Mon, 27 Jul 2026 12:09:26 +0800 Subject: [PATCH 6/8] Fix Windows reader error cleanup --- cpp/src/file/read_file.cc | 4 ++-- cpp/test/cwrapper/c_release_test.cc | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cpp/src/file/read_file.cc b/cpp/src/file/read_file.cc index c6bfd547a..7d41d7095 100644 --- a/cpp/src/file/read_file.cc +++ b/cpp/src/file/read_file.cc @@ -40,7 +40,7 @@ using namespace common; namespace storage { void ReadFile::close() { - if (fd_ > 0) { + if (fd_ >= 0) { ::close(fd_); fd_ = -1; } @@ -65,7 +65,7 @@ int ReadFile::open(const std::string& file_path) { } else if (RET_FAIL(check_file_magic())) { } if (IS_FAIL(ret)) { - ::close(fd_); + close(); } return ret; } diff --git a/cpp/test/cwrapper/c_release_test.cc b/cpp/test/cwrapper/c_release_test.cc index 10e87f3f6..c1fb333ee 100644 --- a/cpp/test/cwrapper/c_release_test.cc +++ b/cpp/test/cwrapper/c_release_test.cc @@ -25,6 +25,7 @@ #endif #include +#include #include #include @@ -89,6 +90,21 @@ TEST_F(CReleaseTest, TestCreateFile) { free_write_file(&file); } +TEST_F(CReleaseTest, RejectCorruptedFileWithoutDoubleClosingDescriptor) { + const char* file_name = "corrupted_empty_file.tsfile"; + remove(file_name); + FILE* empty_file = fopen(file_name, "wb"); + ASSERT_NE(nullptr, empty_file); + ASSERT_EQ(0, fclose(empty_file)); + + ERRNO error_no = RET_OK; + TsFileReader reader = tsfile_reader_new(file_name, &error_no); + EXPECT_EQ(nullptr, reader); + EXPECT_EQ(RET_TSFILE_CORRUPTED, error_no); + + remove(file_name); +} + TEST_F(CReleaseTest, TsFileWriterNew) { ERRNO error_code = RET_OK; From 4df546b126ef8b2eb7fcc48143df28c7be3dbaa3 Mon Sep 17 00:00:00 2001 From: ColinLee Date: Mon, 27 Jul 2026 12:34:05 +0800 Subject: [PATCH 7/8] refactor: remove legacy error codes --- cpp/src/cwrapper/errno_define_c.h | 27 --------------------------- cpp/src/utils/errno_define.h | 18 ------------------ cpp/test/cwrapper/c_release_test.cc | 29 ----------------------------- 3 files changed, 74 deletions(-) diff --git a/cpp/src/cwrapper/errno_define_c.h b/cpp/src/cwrapper/errno_define_c.h index fd167df58..140a90f76 100644 --- a/cpp/src/cwrapper/errno_define_c.h +++ b/cpp/src/cwrapper/errno_define_c.h @@ -57,33 +57,8 @@ #define RET_ENCODE_ERR 53 #define RET_DECODE_ERR 54 -/* - * Deprecated legacy values retained for source compatibility. The current - * TsFile implementation does not return these codes. - */ -#define RET_NET_EPOLL_ERR 9 -#define RET_NET_EPOLL_WAIT_ERR 10 -#define RET_NET_RECV_ERR 11 -#define RET_NET_ACCEPT_ERR 12 -#define RET_NET_FCNTL_ERR 13 -#define RET_NET_LISTEN_ERR 14 -#define RET_NET_SEND_ERR 15 -#define RET_PIPE_ERR 16 -#define RET_THREAD_CREATE_ERR 17 -#define RET_MUTEX_ERR 18 -#define RET_COND_ERR 19 -#define RET_TSBLOCK_TYPE_NOT_SUPPORTED 23 -#define RET_DDL_UNKNOWN_TYPE 25 -#define RET_JSON_INVALID 39 -#define RET_PARSER_ERR 41 -#define RET_ANALYZE_ERR 42 -#define RET_SDK_QUERY_OPTIMIZE_ERR 47 - /* Backward-compatible aliases for identifiers published with misspellings. */ -#define RET_PIPRET_ERR RET_PIPE_ERR -#define RET_THREAD_CREATRET_ERR RET_THREAD_CREATE_ERR #define RET_NO_MORRET_DATA RET_NO_MORE_DATA -#define RET_TSBLOCK_TYPRET_NOT_SUPPORTED RET_TSBLOCK_TYPE_NOT_SUPPORTED #define RET_TYPRET_NOT_SUPPORTED RET_TYPE_NOT_SUPPORTED #define RET_TYPRET_NOT_MATCH RET_TYPE_NOT_MATCH #define RET_FILRET_OPEN_ERR RET_FILE_OPEN_ERR @@ -94,9 +69,7 @@ #define RET_TSFILRET_WRITER_META_ERR RET_TSFILE_WRITER_META_ERR #define RET_FILRET_STAT_ERR RET_FILE_STAT_ERR #define RET_TSFILRET_CORRUPTED RET_TSFILE_CORRUPTED -#define RET_ANALYZRET_ERR RET_ANALYZE_ERR #define RET_DEVICRET_NOT_EXIST RET_DEVICE_NOT_EXIST -#define RET_SDK_QUERY_OPTIMIZRET_ERR RET_SDK_QUERY_OPTIMIZE_ERR #define RET_TABLRET_NOT_EXIST RET_TABLE_NOT_EXIST #define RET_INVALID_NODRET_TYPE RET_INVALID_NODE_TYPE diff --git a/cpp/src/utils/errno_define.h b/cpp/src/utils/errno_define.h index 8bb01235f..ca2d1397a 100644 --- a/cpp/src/utils/errno_define.h +++ b/cpp/src/utils/errno_define.h @@ -58,24 +58,6 @@ const int E_INVALID_NODE_TYPE = 52; const int E_ENCODE_ERR = 53; const int E_DECODE_ERR = 54; -// Deprecated legacy values retained for source compatibility. The current -// TsFile implementation does not return these codes. -const int E_NET_EPOLL_ERR = 9; -const int E_NET_EPOLL_WAIT_ERR = 10; -const int E_NET_RECV_ERR = 11; -const int E_NET_ACCEPT_ERR = 12; -const int E_NET_FCNTL_ERR = 13; -const int E_NET_LISTEN_ERR = 14; -const int E_NET_SEND_ERR = 15; -const int E_PIPE_ERR = 16; -const int E_THREAD_CREATE_ERR = 17; -const int E_MUTEX_ERR = 18; -const int E_COND_ERR = 19; -const int E_JSON_INVALID = 39; -const int E_PARSER_ERR = 41; -const int E_ANALYZE_ERR = 42; -const int E_SDK_QUERY_OPTIMIZE_ERR = 47; - } // end namespace common #endif // UTILS_ERRNO_DEFINE_H diff --git a/cpp/test/cwrapper/c_release_test.cc b/cpp/test/cwrapper/c_release_test.cc index c1fb333ee..ae3f5cb3d 100644 --- a/cpp/test/cwrapper/c_release_test.cc +++ b/cpp/test/cwrapper/c_release_test.cc @@ -39,35 +39,6 @@ extern "C" { namespace CReleaseTest { class CReleaseTest : public testing::Test {}; -TEST_F(CReleaseTest, PublishedErrorCodesRemainSourceCompatible) { - const int published_codes[] = { - RET_NET_EPOLL_ERR, - RET_NET_EPOLL_WAIT_ERR, - RET_NET_RECV_ERR, - RET_NET_ACCEPT_ERR, - RET_NET_FCNTL_ERR, - RET_NET_LISTEN_ERR, - RET_NET_SEND_ERR, - RET_PIPRET_ERR, - RET_THREAD_CREATRET_ERR, - RET_MUTEX_ERR, - RET_COND_ERR, - RET_TSBLOCK_TYPRET_NOT_SUPPORTED, - RET_DDL_UNKNOWN_TYPE, - RET_INVALID_PATH, - RET_JSON_INVALID, - RET_PARSER_ERR, - RET_ANALYZRET_ERR, - RET_SDK_QUERY_OPTIMIZRET_ERR, - }; - const int expected_codes[] = {9, 10, 11, 12, 13, 14, 15, 16, 17, - 18, 19, 23, 25, 37, 39, 41, 42, 47}; - - for (int i = 0; i < 18; ++i) { - EXPECT_EQ(published_codes[i], expected_codes[i]); - } -} - TEST_F(CReleaseTest, TestCreateFile) { ERRNO error_no = RET_OK; remove("create_file1.tsfile"); From 88154813c3e8b1daf671f04cbf687a2f4dea6d29 Mon Sep 17 00:00:00 2001 From: ColinLee Date: Mon, 27 Jul 2026 15:03:10 +0800 Subject: [PATCH 8/8] Remove misspelled C error code aliases --- cpp/src/cwrapper/errno_define_c.h | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/cpp/src/cwrapper/errno_define_c.h b/cpp/src/cwrapper/errno_define_c.h index 140a90f76..3ad1c1301 100644 --- a/cpp/src/cwrapper/errno_define_c.h +++ b/cpp/src/cwrapper/errno_define_c.h @@ -57,20 +57,4 @@ #define RET_ENCODE_ERR 53 #define RET_DECODE_ERR 54 -/* Backward-compatible aliases for identifiers published with misspellings. */ -#define RET_NO_MORRET_DATA RET_NO_MORE_DATA -#define RET_TYPRET_NOT_SUPPORTED RET_TYPE_NOT_SUPPORTED -#define RET_TYPRET_NOT_MATCH RET_TYPE_NOT_MATCH -#define RET_FILRET_OPEN_ERR RET_FILE_OPEN_ERR -#define RET_FILRET_CLOSRET_ERR RET_FILE_CLOSE_ERR -#define RET_FILRET_WRITRET_ERR RET_FILE_WRITE_ERR -#define RET_FILRET_READ_ERR RET_FILE_READ_ERR -#define RET_FILRET_SYNC_ERR RET_FILE_SYNC_ERR -#define RET_TSFILRET_WRITER_META_ERR RET_TSFILE_WRITER_META_ERR -#define RET_FILRET_STAT_ERR RET_FILE_STAT_ERR -#define RET_TSFILRET_CORRUPTED RET_TSFILE_CORRUPTED -#define RET_DEVICRET_NOT_EXIST RET_DEVICE_NOT_EXIST -#define RET_TABLRET_NOT_EXIST RET_TABLE_NOT_EXIST -#define RET_INVALID_NODRET_TYPE RET_INVALID_NODE_TYPE - #endif /* CWRAPPER_ERRNO_DEFINE_H */