diff --git a/cpp/src/cwrapper/tsfile_cwrapper.cc b/cpp/src/cwrapper/tsfile_cwrapper.cc index 8042e10a8..a31e66d1b 100644 --- a/cpp/src/cwrapper/tsfile_cwrapper.cc +++ b/cpp/src/cwrapper/tsfile_cwrapper.cc @@ -737,8 +737,7 @@ ResultSet tsfile_reader_query_tree(TsFileReader reader, char** paths, return nullptr; } *err_code = common::E_INVALID_ARG; - if (reader == nullptr || paths == nullptr || path_num == 0 || - end_time < start_time) { + if (reader == nullptr || paths == nullptr || path_num == 0) { return nullptr; } try { @@ -827,8 +826,7 @@ ResultSet tsfile_reader_query_table(TsFileReader reader, const char* table_name, } *err_code = common::E_INVALID_ARG; if (reader == nullptr || table_name == nullptr || column_names == nullptr || - column_names_len == 0 || end_time < start_time || offset < 0 || - batch_size < 0) { + column_names_len == 0 || offset < 0 || batch_size < 0) { return nullptr; } try { diff --git a/cpp/src/cwrapper/tsfile_cwrapper.h b/cpp/src/cwrapper/tsfile_cwrapper.h index c82a1ae5f..4656c5545 100644 --- a/cpp/src/cwrapper/tsfile_cwrapper.h +++ b/cpp/src/cwrapper/tsfile_cwrapper.h @@ -740,13 +740,19 @@ PreparedSeriesHandle tsfile_reader_prepare_series_with_time_owner( */ void tsfile_prepared_series_free(PreparedSeriesHandle prepared); -/** Query a prepared series without traversing the TsFile footer index. */ +/** + * Query a prepared series without traversing the TsFile footer index. + * An inverted time range succeeds with an empty result set. + */ ResultSet tsfile_reader_query_prepared(TsFileReader reader, PreparedSeriesHandle prepared, Timestamp start_time, Timestamp end_time, int offset, int limit, ERRNO* err_code); -/** Query multiple aligned prepared value columns sharing one time axis. */ +/** + * Query multiple aligned prepared value columns sharing one time axis. + * An inverted time range succeeds with an empty result set. + */ ResultSet tsfile_reader_query_prepared_multi( TsFileReader reader, const PreparedSeriesHandle* prepared, uint32_t prepared_count, Timestamp start_time, Timestamp end_time, @@ -760,7 +766,8 @@ ResultSet tsfile_reader_query_prepared_multi( * @param columns [in] Array of column names to fetch. * @param column_num [in] Number of columns in array. * @param start_time [in] Start timestamp. - * @param end_time [in] End timestamp. Must ≥ start_time. + * @param end_time [in] Inclusive end timestamp. If it precedes start_time, + * the query succeeds with an empty result set. * @return ResultSet Query results handle. Must be freed with * free_tsfile_result_set(). */ @@ -769,6 +776,7 @@ ResultSet tsfile_query_table(TsFileReader reader, const char* table_name, Timestamp start_time, Timestamp end_time, ERRNO* err_code); +/** Query tree-model columns; an inverted time range yields no rows. */ ResultSet tsfile_query_table_on_tree(TsFileReader reader, char** columns, uint32_t column_num, Timestamp start_time, Timestamp end_time, ERRNO* err_code); @@ -780,7 +788,8 @@ ResultSet tsfile_query_table_on_tree(TsFileReader reader, char** columns, * @param paths [in] Array of full paths such as root.device.measurement. * @param path_num [in] Number of paths; must be greater than zero. * @param start_time [in] Inclusive start timestamp. - * @param end_time [in] Inclusive end timestamp. + * @param end_time [in] Inclusive end timestamp. If it precedes start_time, + * the query succeeds with an empty result set. * @param err_code [out] Error code; must not be NULL. * @return ResultSet handle on success, or NULL on failure. */ diff --git a/cpp/src/reader/tsfile_executor.cc b/cpp/src/reader/tsfile_executor.cc index 5ba971e70..3b831dffb 100644 --- a/cpp/src/reader/tsfile_executor.cc +++ b/cpp/src/reader/tsfile_executor.cc @@ -128,7 +128,7 @@ int TsFileExecutor::execute_prepared( ResultSet*& ret_qds) { ASSERT(is_inited_); ret_qds = nullptr; - if (prepared == nullptr || start_time > end_time || offset < 0) { + if (prepared == nullptr || offset < 0) { return E_INVALID_ARG; } auto tsblock_reader = std::unique_ptr( @@ -153,7 +153,7 @@ int TsFileExecutor::execute_prepared_multi( ResultSet*& ret_qds) { ASSERT(is_inited_); ret_qds = nullptr; - if (prepared.empty() || start_time > end_time || offset < 0) { + if (prepared.empty() || offset < 0) { return E_INVALID_ARG; } diff --git a/cpp/src/reader/tsfile_reader.cc b/cpp/src/reader/tsfile_reader.cc index c2f38f5a7..f860a06e5 100644 --- a/cpp/src/reader/tsfile_reader.cc +++ b/cpp/src/reader/tsfile_reader.cc @@ -252,7 +252,7 @@ int TsFileReader::query(const std::string& table_name, schema_it->second == nullptr) { return E_TABLE_NOT_EXIST; } - if (end_time < start_time || offset < 0) { + if (offset < 0) { return E_INVALID_ARG; } if (limit < 0) { diff --git a/cpp/test/reader/prepared_series_test.cc b/cpp/test/reader/prepared_series_test.cc index 0860cb88a..cd1389fbb 100644 --- a/cpp/test/reader/prepared_series_test.cc +++ b/cpp/test/reader/prepared_series_test.cc @@ -245,7 +245,7 @@ TEST_F(PreparedSeriesBatchTest, ResultSet* empty = nullptr; ASSERT_EQ(common::E_OK, - reader.query_prepared(prepared, 100000, 200000, 0, -1, empty)); + reader.query_prepared(prepared, 200000, 100000, 0, -1, empty)); auto* empty_table = dynamic_cast(empty); ASSERT_NE(nullptr, empty_table); block = nullptr; @@ -369,6 +369,18 @@ TEST_F(PreparedSeriesBatchTest, } EXPECT_EQ(10U, row); reader.destroy_query_data_set(result); + + ResultSet* empty_multi = nullptr; + ASSERT_EQ(common::E_OK, + reader.query_prepared_multi({prepared_value2, prepared_value}, 9, + 0, 0, -1, empty_multi)); + auto* empty_multi_table = dynamic_cast(empty_multi); + ASSERT_NE(nullptr, empty_multi_table); + block = nullptr; + EXPECT_EQ(common::E_NO_MORE_DATA, + empty_multi_table->get_next_tsblock(block)); + EXPECT_EQ(nullptr, block); + reader.destroy_query_data_set(empty_multi); EXPECT_EQ(common::E_OK, reader.close()); } diff --git a/cpp/test/reader/table_view/tsfile_reader_table_test.cc b/cpp/test/reader/table_view/tsfile_reader_table_test.cc index d8ce24409..b261f9cec 100644 --- a/cpp/test/reader/table_view/tsfile_reader_table_test.cc +++ b/cpp/test/reader/table_view/tsfile_reader_table_test.cc @@ -288,6 +288,31 @@ TEST_F(TsFileTableReaderTest, TableModelQueryWithTimeFilter) { test_table_model_query(10, 1, 2); } +TEST_F(TsFileTableReaderTest, InvertedTimeRangeReturnsEmptyResult) { + auto table_schema = gen_table_schema(0); + auto tsfile_table_writer = + std::make_shared(&write_file_, table_schema); + auto tablet = gen_tablet(table_schema, 0, 1, 10); + ASSERT_EQ(tsfile_table_writer->write_table(tablet), common::E_OK); + ASSERT_EQ(tsfile_table_writer->flush(), common::E_OK); + ASSERT_EQ(tsfile_table_writer->close(), common::E_OK); + + storage::TsFileReader reader; + ASSERT_EQ(reader.open(file_name_), common::E_OK); + ResultSet* result = nullptr; + ASSERT_EQ( + reader.query(table_schema->get_table_name(), + table_schema->get_measurement_names(), 10, 0, result), + common::E_OK); + auto* table_result = static_cast(result); + bool has_next = false; + ASSERT_EQ(table_result->next(has_next), common::E_OK); + EXPECT_FALSE(has_next); + reader.destroy_query_data_set(table_result); + ASSERT_EQ(reader.close(), common::E_OK); + delete table_schema; +} + TEST_F(TsFileTableReaderTest, TableModelResultMetadata) { auto table_schema = gen_table_schema(0); auto tsfile_table_writer_ = diff --git a/go/tsfile/integration_test.go b/go/tsfile/integration_test.go index e49096bea..4b8d3aafb 100644 --- a/go/tsfile/integration_test.go +++ b/go/tsfile/integration_test.go @@ -171,6 +171,16 @@ func TestTableRoundTripAndNull(t *testing.T) { if got, err := page.Float64(2); err != nil || got != 1.5 { t.Fatalf("page value = %v, %v", got, err) } + + inverted, err := reader.Query("metrics", []string{"value"}, + WithTimeRange(2, 1)) + if err != nil { + t.Fatal(err) + } + defer inverted.Close() + if ok, err := inverted.Next(); err != nil || ok { + t.Fatalf("inverted range Next = %v, %v", ok, err) + } } func TestNewWriterTruncatesExistingFile(t *testing.T) { diff --git a/go/tsfile/reader.go b/go/tsfile/reader.go index 4e1cb3439..2f36e03ad 100644 --- a/go/tsfile/reader.go +++ b/go/tsfile/reader.go @@ -32,11 +32,10 @@ type queryOptions struct { // QueryOption customizes one table query. type QueryOption func(*queryOptions) error +// WithTimeRange limits the query to the inclusive [start, end] range. +// When start is greater than end, the range is empty and the query returns no rows. func WithTimeRange(start, end int64) QueryOption { return func(options *queryOptions) error { - if end < start { - return fmt.Errorf("%w: end must not precede start", ErrInvalidArgument) - } options.start, options.end = start, end return nil } diff --git a/go/tsfile/table_api_contract_test.go b/go/tsfile/table_api_contract_test.go index 65114adf9..a9341670d 100644 --- a/go/tsfile/table_api_contract_test.go +++ b/go/tsfile/table_api_contract_test.go @@ -98,18 +98,17 @@ func TestQueryOptionDefaultsAndComposition(t *testing.T) { } func TestQueryOptionValidation(t *testing.T) { - cases := []QueryOption{ - nil, - WithTimeRange(2, 1), - WithOffset(-1), - WithTagFilter(nil), - } + cases := []QueryOption{nil, WithOffset(-1), WithTagFilter(nil)} for _, option := range cases { if _, err := buildQueryOptions(option); !errors.Is(err, ErrInvalidArgument) { t.Fatalf("option error = %v, want ErrInvalidArgument", err) } } - options, err := buildQueryOptions(WithBatchSize(-1)) + options, err := buildQueryOptions(WithTimeRange(2, 1)) + if err != nil || options.start != 2 || options.end != 1 { + t.Fatalf("inverted time range should be preserved: %+v, %v", options, err) + } + options, err = buildQueryOptions(WithBatchSize(-1)) if err != nil || options.batchSize != 0 { t.Fatalf("negative batch size should select row mode: %+v, %v", options, err) }