This is an automated email from the ASF dual-hosted git repository.
fresh-borzoni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 4a836eae [test] Add Rust integration coverage for RecordBatchLogReader
bounded reads. (#559)
4a836eae is described below
commit 4a836eaeeae49c0bb9fa9057a89f88e6cdd960a3
Author: slfan1989 <[email protected]>
AuthorDate: Tue May 26 01:14:25 2026 +0800
[test] Add Rust integration coverage for RecordBatchLogReader bounded
reads. (#559)
---
crates/fluss/src/client/table/reader.rs | 8 +-
crates/fluss/tests/integration/log_table.rs | 32 +-
.../tests/integration/record_batch_log_reader.rs | 528 +++++++++++++++++++++
crates/fluss/tests/integration/utils.rs | 19 +
crates/fluss/tests/test_fluss.rs | 1 +
5 files changed, 560 insertions(+), 28 deletions(-)
diff --git a/crates/fluss/src/client/table/reader.rs
b/crates/fluss/src/client/table/reader.rs
index 0a08803d..518c68a2 100644
--- a/crates/fluss/src/client/table/reader.rs
+++ b/crates/fluss/src/client/table/reader.rs
@@ -488,10 +488,10 @@ fn filter_batches(
completed
}
-// TODO: Add Rust-level end-to-end tests with `FlussTestingCluster` (feature
-// `integration_tests`) covering `new_until_latest`, partitioned tables,
-// and `new_until_offsets` stopping semantics. Drop cleanup and the
-// reader-active guard are covered by the Python integration test
+// Rust-level end-to-end coverage for `new_until_latest`, partitioned tables,
+// and `new_until_offsets` stopping semantics lives in
+// `crates/fluss/tests/integration/record_batch_log_reader.rs`. Drop cleanup
and the
+// reader-active guard remain covered by the Python integration test
// `test_to_arrow_batch_reader_drop_and_guard`.
#[cfg(test)]
mod tests {
diff --git a/crates/fluss/tests/integration/log_table.rs
b/crates/fluss/tests/integration/log_table.rs
index 5e61ab07..8bad7398 100644
--- a/crates/fluss/tests/integration/log_table.rs
+++ b/crates/fluss/tests/integration/log_table.rs
@@ -20,13 +20,14 @@
mod table_test {
use crate::integration::utils::{
ColumnPlan, array_dt_basics_columns, as_row_type, create_partitions,
create_table,
- dt_array_int, dt_map_string_int, dt_row_seq_label, get_shared_cluster,
make_int_array,
- make_string_array, map_dt_basics_columns, row_dt_basics_columns,
scalar_dt_columns,
+ dt_array_int, dt_map_string_int, dt_row_seq_label,
extract_ids_from_batches,
+ get_shared_cluster, make_int_array, make_string_array,
map_dt_basics_columns,
+ row_dt_basics_columns, scalar_dt_columns,
};
- use arrow::array::{Int32Array, record_batch};
+ use arrow::array::record_batch;
use fluss::client::{EARLIEST_OFFSET, FlussTable, TableScan};
use fluss::metadata::{DataField, DataTypes, Schema, TableDescriptor,
TablePath};
- use fluss::record::{ScanBatch, ScanRecord};
+ use fluss::record::ScanRecord;
use fluss::row::binary_array::FlussArrayWriter;
use fluss::row::binary_map::FlussMapWriter;
use fluss::row::{
@@ -507,30 +508,13 @@ mod table_test {
.unwrap();
writer.flush().await.unwrap();
- fn extract_ids(batches: &[ScanBatch]) -> Vec<i32> {
- batches
- .iter()
- .flat_map(|b| {
- let batch = b.batch();
- (0..batch.num_rows()).map(move |i| {
- batch
- .column(0)
- .as_any()
- .downcast_ref::<Int32Array>()
- .unwrap()
- .value(i)
- })
- })
- .collect()
- }
-
// poll may return partial results if not all batches are available
yet,
// so we accumulate across multiple polls until we have the expected
count.
let mut all_ids = Vec::new();
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
while all_ids.len() < 6 && tokio::time::Instant::now() < deadline {
let batches = scanner.poll(Duration::from_secs(5)).await.unwrap();
- all_ids.extend(extract_ids(&batches));
+ all_ids.extend(extract_ids_from_batches(&batches));
}
// Test 2: Order should be preserved across multiple batches
@@ -547,7 +531,7 @@ mod table_test {
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
while new_ids.len() < 2 && tokio::time::Instant::now() < deadline {
let more = scanner.poll(Duration::from_secs(5)).await.unwrap();
- new_ids.extend(extract_ids(&more));
+ new_ids.extend(extract_ids_from_batches(&more));
}
// Test 3: Subsequent polls should not return duplicate data (offset
continuation)
@@ -561,7 +545,7 @@ mod table_test {
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
while trunc_ids.len() < 5 && tokio::time::Instant::now() < deadline {
let trunc_batches =
trunc_scanner.poll(Duration::from_secs(5)).await.unwrap();
- trunc_ids.extend(extract_ids(&trunc_batches));
+ trunc_ids.extend(extract_ids_from_batches(&trunc_batches));
}
// Subscribing from offset 3 should return [4,5,6,7,8], not
[1,2,3,4,5,6,7,8]
diff --git a/crates/fluss/tests/integration/record_batch_log_reader.rs
b/crates/fluss/tests/integration/record_batch_log_reader.rs
new file mode 100644
index 00000000..13836453
--- /dev/null
+++ b/crates/fluss/tests/integration/record_batch_log_reader.rs
@@ -0,0 +1,528 @@
+/*
+ * 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.
+ */
+
+#[cfg(test)]
+mod reader_test {
+ use crate::integration::utils::{
+ create_partitions, create_table, extract_ids_from_batches,
get_shared_cluster,
+ };
+ use arrow::array::record_batch;
+ use fluss::client::{EARLIEST_OFFSET, FlussConnection,
RecordBatchLogReader};
+ use fluss::config::{Config, NoKeyAssigner};
+ use fluss::metadata::{DataTypes, Schema, TableBucket, TableDescriptor,
TablePath};
+ use fluss::rpc::message::OffsetSpec;
+ use std::collections::HashMap;
+ use std::time::Duration;
+
+ #[tokio::test]
+ async fn until_offsets_stops_at_explicit_offset() {
+ let cluster = get_shared_cluster();
+ let connection = cluster.get_fluss_connection().await;
+ let admin = connection.get_admin().expect("Failed to get admin");
+
+ let table_path = TablePath::new("fluss", "test_reader_until_offsets");
+ let table_descriptor = TableDescriptor::builder()
+ .schema(
+ Schema::builder()
+ .column("id", DataTypes::int())
+ .column("name", DataTypes::string())
+ .build()
+ .expect("Failed to build schema"),
+ )
+ .build()
+ .expect("Failed to build table");
+ create_table(&admin, &table_path, &table_descriptor).await;
+ tokio::time::sleep(Duration::from_secs(1)).await;
+
+ let table = connection
+ .get_table(&table_path)
+ .await
+ .expect("Failed to get table");
+ let writer = table
+ .new_append()
+ .expect("Failed to create append")
+ .create_writer()
+ .expect("Failed to create writer");
+ writer
+ .append_arrow_batch(
+ record_batch!(
+ ("id", Int32, [1, 2, 3, 4, 5, 6]),
+ ("name", Utf8, ["a", "b", "c", "d", "e", "f"])
+ )
+ .unwrap(),
+ )
+ .expect("Failed to append batch");
+ writer.flush().await.expect("Failed to flush");
+
+ let scanner = table
+ .new_scan()
+ .create_record_batch_log_scanner()
+ .expect("Failed to create record batch scanner");
+ scanner
+ .subscribe(0, 1)
+ .await
+ .expect("Failed to subscribe from offset 1");
+
+ let table_id = table.get_table_info().table_id;
+ let mut reader = RecordBatchLogReader::new_until_offsets(
+ scanner,
+ HashMap::from([(TableBucket::new(table_id, 0), 4)]),
+ )
+ .expect("Failed to create record batch reader");
+
+ let batches = tokio::time::timeout(Duration::from_secs(10),
reader.collect_all_batches())
+ .await
+ .expect("Timed out collecting bounded reader batches")
+ .expect("Failed to collect bounded reader batches");
+
+ assert_eq!(
+ extract_ids_from_batches(&batches),
+ vec![2, 3, 4],
+ "reader should include offsets [1, 4) and stop before offset 4"
+ );
+
+ admin
+ .drop_table(&table_path, false)
+ .await
+ .expect("Failed to drop table");
+ }
+
+ #[tokio::test]
+ async fn until_offsets_with_empty_range() {
+ let cluster = get_shared_cluster();
+ let connection = cluster.get_fluss_connection().await;
+ let admin = connection.get_admin().expect("Failed to get admin");
+
+ let table_path = TablePath::new("fluss",
"test_reader_until_offsets_empty_range");
+ let table_descriptor = TableDescriptor::builder()
+ .schema(
+ Schema::builder()
+ .column("id", DataTypes::int())
+ .column("name", DataTypes::string())
+ .build()
+ .expect("Failed to build schema"),
+ )
+ .build()
+ .expect("Failed to build table");
+ create_table(&admin, &table_path, &table_descriptor).await;
+ tokio::time::sleep(Duration::from_secs(1)).await;
+
+ let table = connection
+ .get_table(&table_path)
+ .await
+ .expect("Failed to get table");
+ let writer = table
+ .new_append()
+ .expect("Failed to create append")
+ .create_writer()
+ .expect("Failed to create writer");
+ writer
+ .append_arrow_batch(
+ record_batch!(("id", Int32, [1, 2, 3]), ("name", Utf8, ["a",
"b", "c"])).unwrap(),
+ )
+ .expect("Failed to append batch");
+ writer.flush().await.expect("Failed to flush");
+
+ let scanner = table
+ .new_scan()
+ .create_record_batch_log_scanner()
+ .expect("Failed to create record batch scanner");
+ scanner
+ .subscribe(0, 1)
+ .await
+ .expect("Failed to subscribe from offset 1");
+
+ let table_id = table.get_table_info().table_id;
+ let mut reader = RecordBatchLogReader::new_until_offsets(
+ scanner,
+ HashMap::from([(TableBucket::new(table_id, 0), 1)]),
+ )
+ .expect("Failed to create record batch reader");
+
+ let batches = tokio::time::timeout(Duration::from_secs(10),
reader.collect_all_batches())
+ .await
+ .expect("Timed out collecting empty-range reader batches")
+ .expect("Failed to collect empty-range reader batches");
+
+ assert!(
+ batches.is_empty(),
+ "reader should return no batches when start and stop offsets are
equal"
+ );
+
+ admin
+ .drop_table(&table_path, false)
+ .await
+ .expect("Failed to drop table");
+ }
+
+ #[tokio::test]
+ async fn until_offsets_past_end_of_log() {
+ let cluster = get_shared_cluster();
+ let connection = cluster.get_fluss_connection().await;
+ let admin = connection.get_admin().expect("Failed to get admin");
+
+ let table_path = TablePath::new("fluss",
"test_reader_until_offsets_past_end");
+ let table_descriptor = TableDescriptor::builder()
+ .schema(
+ Schema::builder()
+ .column("id", DataTypes::int())
+ .column("name", DataTypes::string())
+ .build()
+ .expect("Failed to build schema"),
+ )
+ .build()
+ .expect("Failed to build table");
+ create_table(&admin, &table_path, &table_descriptor).await;
+ tokio::time::sleep(Duration::from_secs(1)).await;
+
+ let table = connection
+ .get_table(&table_path)
+ .await
+ .expect("Failed to get table");
+ let writer = table
+ .new_append()
+ .expect("Failed to create append")
+ .create_writer()
+ .expect("Failed to create writer");
+ writer
+ .append_arrow_batch(
+ record_batch!(("id", Int32, [1, 2, 3]), ("name", Utf8, ["a",
"b", "c"])).unwrap(),
+ )
+ .expect("Failed to append initial batch");
+ writer.flush().await.expect("Failed to flush initial batch");
+
+ let scanner = table
+ .new_scan()
+ .create_record_batch_log_scanner()
+ .expect("Failed to create record batch scanner");
+ scanner
+ .subscribe(0, EARLIEST_OFFSET)
+ .await
+ .expect("Failed to subscribe bucket");
+
+ let table_id = table.get_table_info().table_id;
+ let mut reader = RecordBatchLogReader::new_until_offsets(
+ scanner,
+ HashMap::from([(TableBucket::new(table_id, 0), 6)]),
+ )
+ .expect("Failed to create record batch reader");
+
+ let collect_task = tokio::spawn(async move {
reader.collect_all_batches().await });
+ tokio::time::sleep(Duration::from_millis(750)).await;
+ assert!(
+ !collect_task.is_finished(),
+ "reader should wait when the stopping offset is beyond the current
log end"
+ );
+
+ writer
+ .append_arrow_batch(
+ record_batch!(("id", Int32, [4, 5, 6]), ("name", Utf8, ["d",
"e", "f"])).unwrap(),
+ )
+ .expect("Failed to append follow-up batch");
+ writer
+ .flush()
+ .await
+ .expect("Failed to flush follow-up batch");
+
+ let batches = tokio::time::timeout(Duration::from_secs(10),
collect_task)
+ .await
+ .expect("Timed out collecting reader batches after appending past
stop offset")
+ .expect("Reader task panicked")
+ .expect("Failed to collect reader batches");
+
+ assert_eq!(
+ extract_ids_from_batches(&batches),
+ vec![1, 2, 3, 4, 5, 6],
+ "reader should resume after future records arrive and stop at the
requested offset"
+ );
+
+ admin
+ .drop_table(&table_path, false)
+ .await
+ .expect("Failed to drop table");
+ }
+
+ #[tokio::test]
+ async fn until_offsets_multi_bucket() {
+ let cluster = get_shared_cluster();
+ let connection = FlussConnection::new(Config {
+ writer_acks: "all".to_string(),
+ bootstrap_servers:
cluster.plaintext_bootstrap_servers().to_string(),
+ writer_bucket_no_key_assigner: NoKeyAssigner::RoundRobin,
+ ..Default::default()
+ })
+ .await
+ .expect("Failed to connect with round-robin bucket assignment");
+ let admin = connection.get_admin().expect("Failed to get admin");
+
+ let table_path = TablePath::new("fluss",
"test_reader_until_offsets_multi_bucket");
+ let table_descriptor = TableDescriptor::builder()
+ .schema(
+ Schema::builder()
+ .column("id", DataTypes::int())
+ .column("name", DataTypes::string())
+ .build()
+ .expect("Failed to build schema"),
+ )
+ .distributed_by(Some(2), vec!["id".to_string()])
+ .build()
+ .expect("Failed to build table");
+ create_table(&admin, &table_path, &table_descriptor).await;
+ tokio::time::sleep(Duration::from_secs(1)).await;
+
+ let table = connection
+ .get_table(&table_path)
+ .await
+ .expect("Failed to get table");
+ let writer = table
+ .new_append()
+ .expect("Failed to create append")
+ .create_writer()
+ .expect("Failed to create writer");
+ writer
+ .append_arrow_batch(
+ record_batch!(
+ ("id", Int32, [1, 2, 3, 4]),
+ ("name", Utf8, ["a", "b", "c", "d"])
+ )
+ .unwrap(),
+ )
+ .expect("Failed to append first batch");
+ writer
+ .append_arrow_batch(
+ record_batch!(
+ ("id", Int32, [5, 6, 7, 8]),
+ ("name", Utf8, ["e", "f", "g", "h"])
+ )
+ .unwrap(),
+ )
+ .expect("Failed to append second batch");
+ writer.flush().await.expect("Failed to flush");
+
+ let latest_offsets = admin
+ .list_offsets(&table_path, &[0, 1], OffsetSpec::Latest)
+ .await
+ .expect("Failed to list latest offsets");
+ assert!(
+ latest_offsets.values().all(|offset| *offset > 0),
+ "test records should be distributed across both buckets:
{latest_offsets:?}"
+ );
+
+ let scanner = table
+ .new_scan()
+ .create_record_batch_log_scanner()
+ .expect("Failed to create record batch scanner");
+ scanner
+ .subscribe_buckets(&HashMap::from([(0, 0), (1, 0)]))
+ .await
+ .expect("Failed to subscribe to multiple buckets");
+
+ let table_id = table.get_table_info().table_id;
+ let stopping_offsets: HashMap<TableBucket, i64> = latest_offsets
+ .into_iter()
+ .map(|(bucket, offset)| (TableBucket::new(table_id, bucket),
offset))
+ .collect();
+ assert_eq!(
+ stopping_offsets.len(),
+ 2,
+ "reader should track two stopping offsets"
+ );
+
+ let mut reader = RecordBatchLogReader::new_until_offsets(scanner,
stopping_offsets)
+ .expect("Failed to create record batch reader");
+ let batches = tokio::time::timeout(Duration::from_secs(10),
reader.collect_all_batches())
+ .await
+ .expect("Timed out collecting multi-bucket reader batches")
+ .expect("Failed to collect multi-bucket reader batches");
+
+ let mut ids = extract_ids_from_batches(&batches);
+ ids.sort();
+ assert_eq!(ids, vec![1, 2, 3, 4, 5, 6, 7, 8]);
+
+ admin
+ .drop_table(&table_path, false)
+ .await
+ .expect("Failed to drop table");
+ }
+
+ #[tokio::test]
+ async fn until_latest_reads_non_partitioned_table() {
+ let cluster = get_shared_cluster();
+ let connection = cluster.get_fluss_connection().await;
+ let admin = connection.get_admin().expect("Failed to get admin");
+
+ let table_path = TablePath::new("fluss",
"test_reader_non_partitioned_latest");
+ let table_descriptor = TableDescriptor::builder()
+ .schema(
+ Schema::builder()
+ .column("id", DataTypes::int())
+ .column("name", DataTypes::string())
+ .build()
+ .expect("Failed to build schema"),
+ )
+ .build()
+ .expect("Failed to build table");
+
+ create_table(&admin, &table_path, &table_descriptor).await;
+ tokio::time::sleep(Duration::from_secs(1)).await;
+
+ let table = connection
+ .get_table(&table_path)
+ .await
+ .expect("Failed to get table");
+ let writer = table
+ .new_append()
+ .expect("Failed to create append")
+ .create_writer()
+ .expect("Failed to create writer");
+ writer
+ .append_arrow_batch(
+ record_batch!(
+ ("id", Int32, [1, 2, 3, 4]),
+ ("name", Utf8, ["a", "b", "c", "d"])
+ )
+ .unwrap(),
+ )
+ .expect("Failed to append batch");
+ writer.flush().await.expect("Failed to flush");
+
+ let scanner = table
+ .new_scan()
+ .create_record_batch_log_scanner()
+ .expect("Failed to create record batch scanner");
+ scanner
+ .subscribe(0, EARLIEST_OFFSET)
+ .await
+ .expect("Failed to subscribe bucket");
+
+ let mut reader = RecordBatchLogReader::new_until_latest(scanner,
&admin)
+ .await
+ .expect("Failed to create latest-offset reader");
+ let batches = tokio::time::timeout(Duration::from_secs(10),
reader.collect_all_batches())
+ .await
+ .expect("Timed out collecting non-partitioned reader batches")
+ .expect("Failed to collect non-partitioned reader batches");
+
+ assert_eq!(
+ extract_ids_from_batches(&batches),
+ vec![1, 2, 3, 4],
+ "latest-offset reader should read all records present in the
non-partitioned table"
+ );
+
+ admin
+ .drop_table(&table_path, false)
+ .await
+ .expect("Failed to drop table");
+ }
+
+ #[tokio::test]
+ async fn until_latest_reads_partitioned_table() {
+ let cluster = get_shared_cluster();
+ let connection = cluster.get_fluss_connection().await;
+ let admin = connection.get_admin().expect("Failed to get admin");
+
+ let table_path = TablePath::new("fluss",
"test_reader_partitioned_latest");
+ let table_descriptor = TableDescriptor::builder()
+ .schema(
+ Schema::builder()
+ .column("id", DataTypes::int())
+ .column("region", DataTypes::string())
+ .column("value", DataTypes::bigint())
+ .build()
+ .expect("Failed to build schema"),
+ )
+ .partitioned_by(vec!["region"])
+ .build()
+ .expect("Failed to build table");
+
+ create_table(&admin, &table_path, &table_descriptor).await;
+ create_partitions(&admin, &table_path, "region", &["US", "EU"]).await;
+ tokio::time::sleep(Duration::from_secs(2)).await;
+
+ let table = connection
+ .get_table(&table_path)
+ .await
+ .expect("Failed to get table");
+ let writer = table
+ .new_append()
+ .expect("Failed to create append")
+ .create_writer()
+ .expect("Failed to create writer");
+
+ let us_batch = record_batch!(
+ ("id", Int32, [1, 2]),
+ ("region", Utf8, ["US", "US"]),
+ ("value", Int64, [100, 200])
+ )
+ .unwrap();
+ writer
+ .append_arrow_batch(us_batch)
+ .expect("Failed to append US batch");
+
+ let eu_batch = record_batch!(
+ ("id", Int32, [3, 4]),
+ ("region", Utf8, ["EU", "EU"]),
+ ("value", Int64, [300, 400])
+ )
+ .unwrap();
+ writer
+ .append_arrow_batch(eu_batch)
+ .expect("Failed to append EU batch");
+ writer.flush().await.expect("Failed to flush");
+
+ let scanner = table
+ .new_scan()
+ .create_record_batch_log_scanner()
+ .expect("Failed to create record batch scanner");
+ for partition in admin
+ .list_partition_infos(&table_path)
+ .await
+ .expect("Failed to list partition infos")
+ {
+ // The table uses the default single-bucket layout, so bucket 0 is
+ // the only bucket in each partition. If this test switches to a
+ // multi-bucket table, subscribe all buckets for each partition.
+ scanner
+ .subscribe_partition(partition.get_partition_id(), 0,
EARLIEST_OFFSET)
+ .await
+ .expect("Failed to subscribe partition bucket");
+ }
+
+ let mut reader = RecordBatchLogReader::new_until_latest(scanner,
&admin)
+ .await
+ .expect("Failed to create latest-offset reader");
+ let batches = tokio::time::timeout(Duration::from_secs(10),
reader.collect_all_batches())
+ .await
+ .expect("Timed out collecting partitioned reader batches")
+ .expect("Failed to collect partitioned reader batches");
+
+ let mut ids = extract_ids_from_batches(&batches);
+ ids.sort();
+ assert_eq!(
+ ids,
+ vec![1, 2, 3, 4],
+ "latest-offset reader should read all records present in
subscribed partitions"
+ );
+
+ admin
+ .drop_table(&table_path, false)
+ .await
+ .expect("Failed to drop table");
+ }
+}
diff --git a/crates/fluss/tests/integration/utils.rs
b/crates/fluss/tests/integration/utils.rs
index 58c77f03..934b6626 100644
--- a/crates/fluss/tests/integration/utils.rs
+++ b/crates/fluss/tests/integration/utils.rs
@@ -16,10 +16,12 @@
* limitations under the License.
*/
use crate::integration::fluss_cluster::{FlussTestingCluster,
FlussTestingClusterBuilder};
+use arrow::array::Int32Array;
use fluss::client::FlussAdmin;
use fluss::metadata::{
DataField, DataType, DataTypes, PartitionSpec, RowType, Schema,
TableDescriptor, TablePath,
};
+use fluss::record::ScanBatch;
use fluss::row::FlussArray;
use fluss::row::binary_array::FlussArrayWriter;
use std::collections::HashMap;
@@ -120,6 +122,23 @@ pub fn make_int_array(values: &[Option<i32>]) ->
FlussArray {
writer.complete().expect("Failed to build int array")
}
+pub fn extract_ids_from_batches(batches: &[ScanBatch]) -> Vec<i32> {
+ batches
+ .iter()
+ .flat_map(|scan_batch| {
+ let batch = scan_batch.batch();
+ (0..batch.num_rows()).map(move |row| {
+ batch
+ .column(0)
+ .as_any()
+ .downcast_ref::<Int32Array>()
+ .expect("id column should be Int32")
+ .value(row)
+ })
+ })
+ .collect()
+}
+
/// Similar to wait_for_cluster_ready but connects with SASL credentials.
pub async fn wait_for_cluster_ready_with_sasl(cluster: &FlussTestingCluster) {
let timeout = Duration::from_secs(30);
diff --git a/crates/fluss/tests/test_fluss.rs b/crates/fluss/tests/test_fluss.rs
index 96756469..792b68f0 100644
--- a/crates/fluss/tests/test_fluss.rs
+++ b/crates/fluss/tests/test_fluss.rs
@@ -24,6 +24,7 @@ mod integration {
mod fluss_cluster;
mod kv_table;
mod log_table;
+ mod record_batch_log_reader;
mod sasl_auth;
mod utils;