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

zhangstar333 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 1731787677f [chore](lance) update lance version to tag 0.1.7 (#67115)
1731787677f is described below

commit 1731787677f0199ccdb4fe6318f9116310627c52
Author: zhangstar333 <[email protected]>
AuthorDate: Tue Aug 25 22:36:06 2026 +0800

    [chore](lance) update lance version to tag 0.1.7 (#67115)
    
    ### What problem does this PR solve?
    Problem Summary:
    1.  update lance version to tag 0.1.7
    2. group lance scan parameters in Thrift
---
 gensrc/thrift/PlanNodes.thrift               |   13 +-
 thirdparty/download-thirdparty.sh            |    6 +-
 thirdparty/patches/lance-c-0.1.6-doris.patch |  632 -----------
 thirdparty/patches/lance-c-0.1.7-pr-64.patch | 1522 ++++++++++++++++++++++++++
 thirdparty/vars.sh                           |    8 +-
 5 files changed, 1536 insertions(+), 645 deletions(-)

diff --git a/gensrc/thrift/PlanNodes.thrift b/gensrc/thrift/PlanNodes.thrift
index b2702dd0ed4..1eaa53251bc 100644
--- a/gensrc/thrift/PlanNodes.thrift
+++ b/gensrc/thrift/PlanNodes.thrift
@@ -497,6 +497,12 @@ struct TExternalSearchRequest {
     1: optional i32 schema_version = 1
 }
 
+struct TLanceScanParams {
+    1: optional binary lance_substrait_filter
+    2: optional TExternalSearchRequest external_search_request
+    3: optional map<string, string> lance_storage_options
+}
+
 struct TFileScanRangeParams {
     // deprecated, move to TFileScanRange
     1: optional Types.TFileType file_type;
@@ -580,12 +586,7 @@ struct TFileScanRangeParams {
     // HMS catalog property hive.parquet.time-zone. When absent, format_v2 
keeps INT96 wall-clock
     // values unchanged. When present, only INT96 TIMESTAMP values are 
converted with this zone.
     36: optional string hive_parquet_time_zone
-    // Serialized Substrait ExtendedExpression executed by the native Lance 
scanner. Set at
-    // ScanNode level so it is not serialized once per fragment split.
-    37: optional binary lance_substrait_filter
-    // Provider-independent search request. Set at ScanNode level so all 
ranges use the same logical
-    // query. The first implementation uses one whole-dataset range for Lance 
vector search.
-    38: optional TExternalSearchRequest external_search_request
+    37: optional TLanceScanParams lance_scan_params
 }
 
 struct TFileRangeDesc {
diff --git a/thirdparty/download-thirdparty.sh 
b/thirdparty/download-thirdparty.sh
index 830da72eacf..8d65b8ce275 100755
--- a/thirdparty/download-thirdparty.sh
+++ b/thirdparty/download-thirdparty.sh
@@ -819,12 +819,12 @@ if [[ " ${TP_ARCHIVES[*]} " =~ " PAIMON_CPP " ]]; then
     echo "Finished patching ${PAIMON_CPP_SOURCE}"
 fi
 
-# Patch lance-c for fragment-scoped nearest-neighbor search and row-ID-based 
fetching.
+# Patch lance-c with the scan execution statistics API from upstream PR #64.
 if [[ " ${TP_ARCHIVES[*]} " =~ " LANCE_C " ]]; then
-    if [[ "${LANCE_C_SOURCE}" == "lance-c-0.1.6" ]]; then
+    if [[ "${LANCE_C_SOURCE}" == "lance-c-0.1.7" ]]; then
         cd "${TP_SOURCE_DIR}/${LANCE_C_SOURCE}"
         if [[ ! -f "${PATCHED_MARK}" ]]; then
-            patch -p1 <"${TP_PATCH_DIR}/lance-c-0.1.6-doris.patch"
+            patch -p1 <"${TP_PATCH_DIR}/lance-c-0.1.7-pr-64.patch"
             touch "${PATCHED_MARK}"
         fi
         cd -
diff --git a/thirdparty/patches/lance-c-0.1.6-doris.patch 
b/thirdparty/patches/lance-c-0.1.6-doris.patch
deleted file mode 100644
index cc49293d9de..00000000000
--- a/thirdparty/patches/lance-c-0.1.6-doris.patch
+++ /dev/null
@@ -1,632 +0,0 @@
-diff --git a/include/lance/lance.h b/include/lance/lance.h
-index 1de72fa..a0a8b5e 100644
---- a/include/lance/lance.h
-+++ b/include/lance/lance.h
-@@ -752,0 +753,25 @@ int32_t lance_dataset_take(
-+/**
-+ * Take rows by dataset row IDs.
-+ *
-+ * Row IDs are values from the `_rowid` scanner column, not zero-based row
-+ * offsets. They must belong to the same dataset snapshot used for this read.
-+ * Missing or deleted row IDs may be omitted from the result. For found rows,
-+ * input order and duplicates are preserved.
-+ *
-+ * @param dataset      Open dataset snapshot.
-+ * @param row_ids      Array of dataset row IDs. May be NULL only when
-+ *                     `num_row_ids` is zero.
-+ * @param num_row_ids  Length of `row_ids`.
-+ * @param columns      NULL-terminated column names, or NULL for all. The
-+ *                     system column `_rowid` may be requested explicitly.
-+ * @param out          Pointer to caller-allocated ArrowArrayStream.
-+ * @return 0 on success, -1 on error.
-+ */
-+int32_t lance_dataset_take_rows(
-+    const LanceDataset* dataset,
-+    const uint64_t* row_ids,
-+    size_t num_row_ids,
-+    const char* const* columns,
-+    struct ArrowArrayStream* out
-+);
-+
-diff --git a/include/lance/lance.hpp b/include/lance/lance.hpp
-index 40aa9e3..d54dc5a 100644
---- a/include/lance/lance.hpp
-+++ b/include/lance/lance.hpp
-@@ -654,0 +655,24 @@ public:
-+    /// Take rows by dataset row IDs. Results exported as ArrowArrayStream.
-+    void take_rows(const uint64_t* row_ids, size_t num_row_ids,
-+                   const std::vector<std::string>& columns,
-+                   ArrowArrayStream* out) const {
-+        std::vector<const char*> col_ptrs;
-+        for (auto& c : columns) col_ptrs.push_back(c.c_str());
-+        col_ptrs.push_back(nullptr);
-+        const char* const* cols_ptr = columns.empty() ? nullptr : 
col_ptrs.data();
-+
-+        if (lance_dataset_take_rows(
-+                handle_.get(), row_ids, num_row_ids, cols_ptr, out) != 0) {
-+            check_error();
-+        }
-+    }
-+
-+    /// Take all columns by dataset row IDs.
-+    void take_rows(const uint64_t* row_ids, size_t num_row_ids,
-+                   ArrowArrayStream* out) const {
-+        if (lance_dataset_take_rows(
-+                handle_.get(), row_ids, num_row_ids, nullptr, out) != 0) {
-+            check_error();
-+        }
-+    }
-+
-diff --git a/src/dataset.rs b/src/dataset.rs
-index 91a735f..364be09 100644
---- a/src/dataset.rs
-+++ b/src/dataset.rs
-@@ -44,0 +45,20 @@ impl LanceDataset {
-+fn projection_from_columns(
-+    dataset: &Dataset,
-+    columns: Option<&[String]>,
-+) -> Result<lance::dataset::ProjectionRequest> {
-+    match columns {
-+        Some(columns) => {
-+            let schema = dataset
-+                .schema()
-+                .project_preserve_system_columns(columns)
-+                .map_err(|err| {
-+                    lance_core::Error::invalid_input(format!("invalid columns 
{columns:?}: {err}"))
-+                })?;
-+            Ok(lance::dataset::ProjectionRequest::from_schema(schema))
-+        }
-+        None => Ok(lance::dataset::ProjectionRequest::from_schema(
-+            dataset.schema().clone(),
-+        )),
-+    }
-+}
-+
-@@ -247,4 +267 @@ unsafe fn dataset_take_inner(
--    let projection = match &col_names {
--        Some(cols) => 
lance::dataset::ProjectionRequest::from_columns(cols.iter(), snap.schema()),
--        None => 
lance::dataset::ProjectionRequest::from_schema(snap.schema().clone()),
--    };
-+    let projection = projection_from_columns(&snap, col_names.as_deref())?;
-@@ -263,0 +281,69 @@ unsafe fn dataset_take_inner(
-+/// Take rows by dataset row IDs, returning results as an ArrowArrayStream.
-+///
-+/// - `row_ids`: array of dataset row IDs, such as values returned in the
-+///   `_rowid` scanner column
-+/// - `num_row_ids`: length of the row ID array
-+/// - `columns`: NULL-terminated column name array, or NULL for all columns
-+/// - `out`: pointer to a stack-allocated `ArrowArrayStream`
-+///
-+/// `row_ids` may be NULL only when `num_row_ids` is zero. Row IDs must belong
-+/// to the same dataset snapshot used for this read. Missing or deleted row 
IDs
-+/// may be omitted from the result by the upstream Lance implementation.
-+///
-+/// Returns 0 on success, -1 on error.
-+#[unsafe(no_mangle)]
-+pub unsafe extern "C" fn lance_dataset_take_rows(
-+    dataset: *const LanceDataset,
-+    row_ids: *const u64,
-+    num_row_ids: usize,
-+    columns: *const *const c_char,
-+    out: *mut FFI_ArrowArrayStream,
-+) -> i32 {
-+    ffi_try!(
-+        unsafe { dataset_take_rows_inner(dataset, row_ids, num_row_ids, 
columns, out) },
-+        neg
-+    )
-+}
-+
-+unsafe fn dataset_take_rows_inner(
-+    dataset: *const LanceDataset,
-+    row_ids: *const u64,
-+    num_row_ids: usize,
-+    columns: *const *const c_char,
-+    out: *mut FFI_ArrowArrayStream,
-+) -> Result<i32> {
-+    if dataset.is_null() {
-+        return Err(lance_core::Error::invalid_input("dataset must not be 
NULL"));
-+    }
-+    if out.is_null() {
-+        return Err(lance_core::Error::invalid_input("out must not be NULL"));
-+    }
-+    if num_row_ids > 0 && row_ids.is_null() {
-+        return Err(lance_core::Error::invalid_input(format!(
-+            "row_ids must not be NULL when num_row_ids = {num_row_ids}"
-+        )));
-+    }
-+
-+    let ds = unsafe { &*dataset };
-+    let row_id_slice = if num_row_ids == 0 {
-+        &[]
-+    } else {
-+        unsafe { std::slice::from_raw_parts(row_ids, num_row_ids) }
-+    };
-+    let col_names = unsafe { helpers::parse_c_string_array(columns)? };
-+
-+    let snap = ds.snapshot();
-+    let projection = projection_from_columns(&snap, col_names.as_deref())?;
-+
-+    let batch = block_on(snap.take_rows(row_id_slice, projection))?;
-+
-+    // Match lance_dataset_take: export the single RecordBatch as an Arrow 
stream.
-+    let schema = batch.schema();
-+    let reader = 
arrow::record_batch::RecordBatchIterator::new(vec![Ok(batch)], schema);
-+    let ffi_stream = FFI_ArrowArrayStream::new(Box::new(reader));
-+    unsafe {
-+        std::ptr::write_unaligned(out, ffi_stream);
-+    }
-+    Ok(0)
-+}
-+
-diff --git a/src/scanner.rs b/src/scanner.rs
-index c1c2f67..de646be 100644
---- a/src/scanner.rs
-+++ b/src/scanner.rs
-@@ -160,0 +161,6 @@ impl LanceScanner {
-+        // Lance validates fragment-scoped nearest searches when nearest() is
-+        // configured. Such searches are supported when the fragment scan is
-+        // the input to a prefilter, so this flag must be set first.
-+        if self.prefilter {
-+            scanner.prefilter(true);
-+        }
-@@ -178,3 +183,0 @@ impl LanceScanner {
--            if self.prefilter {
--                scanner.prefilter(true);
--            }
-@@ -221,0 +225,5 @@ impl LanceScanner {
-+        // nearest() checks the current prefilter setting before accepting a
-+        // fragment-scoped search. Enable it before installing the query.
-+        if self.prefilter {
-+            scanner.prefilter(true);
-+        }
-@@ -239,3 +246,0 @@ impl LanceScanner {
--            if self.prefilter {
--                scanner.prefilter(true);
--            }
-diff --git a/tests/c_api_test.rs b/tests/c_api_test.rs
-index 2babda6..9ee5af9 100644
---- a/tests/c_api_test.rs
-+++ b/tests/c_api_test.rs
-@@ -9,0 +10 @@ use std::ffi::{CString, c_char};
-+use std::process::Command;
-@@ -18 +19 @@ use arrow::record_batch::RecordBatchReader;
--use arrow_array::{Array, Float32Array, Int32Array, RecordBatch, StringArray};
-+use arrow_array::{Array, Float32Array, Int32Array, RecordBatch, StringArray, 
UInt64Array};
-@@ -393,0 +395,94 @@ fn test_dataset_take() {
-+#[test]
-+fn test_dataset_take_rows_empty_and_null_validation() {
-+    let (_tmp, uri) = create_test_dataset();
-+    let c_uri = c_str(&uri);
-+    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
-+    assert!(!ds.is_null());
-+
-+    let mut empty_stream = FFI_ArrowArrayStream::empty();
-+    assert_eq!(
-+        unsafe { lance_dataset_take_rows(ds, ptr::null(), 0, ptr::null(), 
&mut empty_stream) },
-+        0
-+    );
-+    let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut empty_stream) 
}.unwrap();
-+    assert_eq!(
-+        reader.map(|batch| batch.unwrap().num_rows()).sum::<usize>(),
-+        0
-+    );
-+
-+    let mut invalid_stream = FFI_ArrowArrayStream::empty();
-+    assert_eq!(
-+        unsafe { lance_dataset_take_rows(ds, ptr::null(), 1, ptr::null(), 
&mut invalid_stream) },
-+        -1
-+    );
-+    let message = unsafe { 
std::ffi::CStr::from_ptr(lance_last_error_message()) }.to_string_lossy();
-+    assert!(
-+        message.contains("row_ids must not be NULL when num_row_ids = 1"),
-+        "unexpected error: {message}"
-+    );
-+
-+    let row_id = 0_u64;
-+    assert_eq!(
-+        unsafe {
-+            lance_dataset_take_rows(ptr::null(), &row_id, 1, ptr::null(), 
&mut invalid_stream)
-+        },
-+        -1
-+    );
-+    assert_eq!(
-+        unsafe { lance_dataset_take_rows(ds, &row_id, 1, ptr::null(), 
ptr::null_mut()) },
-+        -1
-+    );
-+
-+    unsafe { lance_dataset_close(ds) };
-+}
-+
-+#[test]
-+fn test_dataset_take_rows_invalid_column() {
-+    const CHILD_ENV: &str = "LANCE_C_TEST_TAKE_ROWS_INVALID_COLUMN_CHILD";
-+
-+    if std::env::var_os(CHILD_ENV).is_some() {
-+        let (_tmp, uri) = create_test_dataset();
-+        let c_uri = c_str(&uri);
-+        let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) 
};
-+        assert!(!ds.is_null());
-+
-+        let row_id = 0_u64;
-+        let invalid_column = c_str("unknown_column");
-+        let columns = [invalid_column.as_ptr(), ptr::null()];
-+        let mut stream = FFI_ArrowArrayStream::empty();
-+        assert_eq!(
-+            unsafe { lance_dataset_take_rows(ds, &row_id, 1, 
columns.as_ptr(), &mut stream) },
-+            -1
-+        );
-+        assert_eq!(lance_last_error_code(), LanceErrorCode::InvalidArgument);
-+
-+        let message_ptr = lance_last_error_message();
-+        assert!(!message_ptr.is_null());
-+        let message = unsafe { std::ffi::CStr::from_ptr(message_ptr) }
-+            .to_string_lossy()
-+            .into_owned();
-+        unsafe { lance_free_string(message_ptr) };
-+        assert!(
-+            message.contains("unknown_column"),
-+            "unexpected error: {message}"
-+        );
-+
-+        unsafe { lance_dataset_close(ds) };
-+        return;
-+    }
-+
-+    let output = Command::new(std::env::current_exe().unwrap())
-+        .arg("--exact")
-+        .arg("test_dataset_take_rows_invalid_column")
-+        .arg("--nocapture")
-+        .env(CHILD_ENV, "1")
-+        .output()
-+        .unwrap();
-+    assert!(
-+        output.status.success(),
-+        "invalid-column subprocess failed\nstdout:\n{}\nstderr:\n{}",
-+        String::from_utf8_lossy(&output.stdout),
-+        String::from_utf8_lossy(&output.stderr)
-+    );
-+}
-+
-@@ -2497,0 +2593,73 @@ fn create_vector_dataset(num_rows: i32, dim: i32) -> 
(tempfile::TempDir, String)
-+/// Create two vector fragments with deterministic vectors. Every component of
-+/// row `id` is `id as f32`, making nearest-neighbor expectations unambiguous.
-+fn create_multi_fragment_vector_dataset(
-+    rows_per_fragment: i32,
-+    dim: i32,
-+    enable_stable_row_ids: bool,
-+) -> (tempfile::TempDir, String) {
-+    use arrow_array::builder::{FixedSizeListBuilder, Float32Builder};
-+
-+    let tmp = tempfile::tempdir().unwrap();
-+    let uri = tmp
-+        .path()
-+        .join("multi_fragment_vec_ds")
-+        .to_str()
-+        .unwrap()
-+        .to_string();
-+    let schema = Arc::new(Schema::new(vec![
-+        Field::new("id", DataType::Int32, false),
-+        Field::new(
-+            "embedding",
-+            DataType::FixedSizeList(Arc::new(Field::new("item", 
DataType::Float32, true)), dim),
-+            false,
-+        ),
-+    ]));
-+
-+    let make_batch = |first_id: i32| {
-+        let ids: Vec<i32> = (first_id..first_id + 
rows_per_fragment).collect();
-+        let mut embeddings = FixedSizeListBuilder::new(Float32Builder::new(), 
dim);
-+        for id in &ids {
-+            for _ in 0..dim {
-+                embeddings.values().append_value(*id as f32);
-+            }
-+            embeddings.append(true);
-+        }
-+        RecordBatch::try_new(
-+            schema.clone(),
-+            vec![
-+                Arc::new(Int32Array::from(ids)),
-+                Arc::new(embeddings.finish()),
-+            ],
-+        )
-+        .unwrap()
-+    };
-+
-+    let first = make_batch(0);
-+    let second = make_batch(rows_per_fragment);
-+    lance_c::runtime::block_on(async {
-+        Dataset::write(
-+            arrow::record_batch::RecordBatchIterator::new(vec![Ok(first)], 
schema.clone()),
-+            &uri,
-+            Some(lance::dataset::WriteParams {
-+                enable_stable_row_ids,
-+                ..Default::default()
-+            }),
-+        )
-+        .await
-+        .unwrap();
-+        Dataset::write(
-+            arrow::record_batch::RecordBatchIterator::new(vec![Ok(second)], 
schema),
-+            &uri,
-+            Some(lance::dataset::WriteParams {
-+                mode: lance::dataset::WriteMode::Append,
-+                enable_stable_row_ids,
-+                ..Default::default()
-+            }),
-+        )
-+        .await
-+        .unwrap();
-+    });
-+
-+    (tmp, uri)
-+}
-+
-@@ -2723,0 +2892,99 @@ fn test_scanner_nearest_brute_force() {
-+fn 
assert_dataset_take_rows_from_multi_fragment_ann_result(enable_stable_row_ids: 
bool) {
-+    let (_tmp, uri) = create_multi_fragment_vector_dataset(32, 8, 
enable_stable_row_ids);
-+    let uri_c = c_str(&uri);
-+    let ds = unsafe { lance_dataset_open(uri_c.as_ptr(), ptr::null(), 0) };
-+    assert!(!ds.is_null());
-+
-+    // A non-NULL array whose first element is NULL is an explicit empty
-+    // projection. The ANN result should therefore contain only _distance and
-+    // the explicitly requested _rowid system column.
-+    let no_columns: [*const c_char; 1] = [ptr::null()];
-+    let scanner = unsafe { lance_scanner_new(ds, no_columns.as_ptr(), 
ptr::null()) };
-+    assert!(!scanner.is_null());
-+    assert_eq!(unsafe { lance_scanner_with_row_id(scanner, true) }, 0);
-+
-+    let column = c_str("embedding");
-+    let query = [40.0_f32; 8];
-+    assert_eq!(
-+        unsafe {
-+            lance_scanner_nearest(
-+                scanner,
-+                column.as_ptr(),
-+                query.as_ptr().cast(),
-+                query.len(),
-+                LanceDataType::Float32 as i32,
-+                1,
-+            )
-+        },
-+        0
-+    );
-+    assert_eq!(unsafe { lance_scanner_set_use_index(scanner, false) }, 0);
-+
-+    let mut ann_stream = FFI_ArrowArrayStream::empty();
-+    assert_eq!(
-+        unsafe { lance_scanner_to_arrow_stream(scanner, &mut ann_stream) },
-+        0
-+    );
-+    let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut ann_stream) 
}.unwrap();
-+    let ann_batches = reader.map(|batch| batch.unwrap()).collect::<Vec<_>>();
-+    assert_eq!(
-+        ann_batches.iter().map(RecordBatch::num_rows).sum::<usize>(),
-+        1
-+    );
-+    assert_eq!(ann_batches[0].num_columns(), 2);
-+
-+    let distance = ann_batches[0]
-+        .column_by_name("_distance")
-+        .unwrap()
-+        .as_any()
-+        .downcast_ref::<Float32Array>()
-+        .unwrap()
-+        .value(0);
-+    assert_eq!(distance, 0.0);
-+    let row_id = ann_batches[0]
-+        .column_by_name("_rowid")
-+        .unwrap()
-+        .as_any()
-+        .downcast_ref::<UInt64Array>()
-+        .unwrap()
-+        .value(0);
-+    if !enable_stable_row_ids {
-+        assert_ne!(
-+            row_id >> 32,
-+            0,
-+            "expected an address-style row ID from the second fragment"
-+        );
-+    }
-+
-+    let id_column = c_str("id");
-+    let columns = [id_column.as_ptr(), ptr::null()];
-+    let mut take_stream = FFI_ArrowArrayStream::empty();
-+    assert_eq!(
-+        unsafe { lance_dataset_take_rows(ds, &row_id, 1, columns.as_ptr(), 
&mut take_stream) },
-+        0
-+    );
-+    let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut take_stream) 
}.unwrap();
-+    let batches = reader.map(|batch| batch.unwrap()).collect::<Vec<_>>();
-+    assert_eq!(batches.len(), 1);
-+    let ids = batches[0]
-+        .column_by_name("id")
-+        .unwrap()
-+        .as_any()
-+        .downcast_ref::<Int32Array>()
-+        .unwrap();
-+    assert_eq!(ids.values(), &[40]);
-+
-+    unsafe { lance_scanner_close(scanner) };
-+    unsafe { lance_dataset_close(ds) };
-+}
-+
-+#[test]
-+fn test_dataset_take_rows_from_multi_fragment_ann_result() {
-+    assert_dataset_take_rows_from_multi_fragment_ann_result(false);
-+}
-+
-+#[test]
-+fn 
test_dataset_take_rows_from_multi_fragment_ann_result_with_stable_row_ids() {
-+    assert_dataset_take_rows_from_multi_fragment_ann_result(true);
-+}
-+
-@@ -2855,0 +3123,132 @@ fn test_scanner_nearest_filter_postfilter() {
-+#[test]
-+fn test_scanner_nearest_prefilter_with_fragment_ids_next() {
-+    let (_tmp, uri) = create_multi_fragment_vector_dataset(32, 8, false);
-+    let uri_c = c_str(&uri);
-+    let ds = unsafe { lance_dataset_open(uri_c.as_ptr(), ptr::null(), 0) };
-+    assert!(!ds.is_null());
-+
-+    let mut fragment_ids = vec![0; unsafe { lance_dataset_fragment_count(ds) 
} as usize];
-+    assert_eq!(fragment_ids.len(), 2);
-+    assert_eq!(
-+        unsafe { lance_dataset_fragment_ids(ds, fragment_ids.as_mut_ptr()) },
-+        0
-+    );
-+
-+    let filter = c_str("id >= 40");
-+    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), 
filter.as_ptr()) };
-+    assert_eq!(
-+        unsafe { lance_scanner_set_fragment_ids(scanner, 
fragment_ids[1..].as_ptr(), 1) },
-+        0
-+    );
-+
-+    // Match the Doris call order: nearest is configured before prefilter.
-+    let column = c_str("embedding");
-+    let query = [40.0_f32; 8];
-+    assert_eq!(
-+        unsafe {
-+            lance_scanner_nearest(
-+                scanner,
-+                column.as_ptr(),
-+                query.as_ptr().cast(),
-+                query.len(),
-+                LanceDataType::Float32 as i32,
-+                5,
-+            )
-+        },
-+        0
-+    );
-+    assert_eq!(unsafe { lance_scanner_set_prefilter(scanner, true) }, 0);
-+    assert_eq!(unsafe { lance_scanner_set_use_index(scanner, false) }, 0);
-+
-+    let batches = scan_all_rows_from_scanner(scanner);
-+    let mut ids = batches
-+        .iter()
-+        .flat_map(|batch| {
-+            batch
-+                .column_by_name("id")
-+                .unwrap()
-+                .as_any()
-+                .downcast_ref::<Int32Array>()
-+                .unwrap()
-+                .values()
-+                .iter()
-+                .copied()
-+                .collect::<Vec<_>>()
-+        })
-+        .collect::<Vec<_>>();
-+    ids.sort_unstable();
-+    assert_eq!(ids, vec![40, 41, 42, 43, 44]);
-+
-+    unsafe { lance_scanner_close(scanner) };
-+    unsafe { lance_dataset_close(ds) };
-+}
-+
-+#[test]
-+fn test_scanner_nearest_prefilter_with_fragment_ids_arrow_stream() {
-+    let (_tmp, uri) = create_multi_fragment_vector_dataset(32, 8, false);
-+    let uri_c = c_str(&uri);
-+    let ds = unsafe { lance_dataset_open(uri_c.as_ptr(), ptr::null(), 0) };
-+    assert!(!ds.is_null());
-+
-+    let mut fragment_ids = vec![0; unsafe { lance_dataset_fragment_count(ds) 
} as usize];
-+    assert_eq!(fragment_ids.len(), 2);
-+    assert_eq!(
-+        unsafe { lance_dataset_fragment_ids(ds, fragment_ids.as_mut_ptr()) },
-+        0
-+    );
-+
-+    let filter = c_str("id >= 60");
-+    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), 
filter.as_ptr()) };
-+    assert_eq!(
-+        unsafe { lance_scanner_set_fragment_ids(scanner, 
fragment_ids[1..].as_ptr(), 1) },
-+        0
-+    );
-+
-+    let column = c_str("embedding");
-+    let query = [60.0_f32; 8];
-+    assert_eq!(
-+        unsafe {
-+            lance_scanner_nearest(
-+                scanner,
-+                column.as_ptr(),
-+                query.as_ptr().cast(),
-+                query.len(),
-+                LanceDataType::Float32 as i32,
-+                10,
-+            )
-+        },
-+        0
-+    );
-+    assert_eq!(unsafe { lance_scanner_set_prefilter(scanner, true) }, 0);
-+    assert_eq!(unsafe { lance_scanner_set_use_index(scanner, false) }, 0);
-+
-+    let mut stream = FFI_ArrowArrayStream::empty();
-+    assert_eq!(
-+        unsafe { lance_scanner_to_arrow_stream(scanner, &mut stream) },
-+        0,
-+        "{}",
-+        unsafe { std::ffi::CStr::from_ptr(lance_last_error_message()) 
}.to_string_lossy()
-+    );
-+    let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut stream) 
}.unwrap();
-+    let mut ids = reader
-+        .flat_map(|batch| {
-+            let batch = batch.unwrap();
-+            batch
-+                .column_by_name("id")
-+                .unwrap()
-+                .as_any()
-+                .downcast_ref::<Int32Array>()
-+                .unwrap()
-+                .values()
-+                .iter()
-+                .copied()
-+                .collect::<Vec<_>>()
-+        })
-+        .collect::<Vec<_>>();
-+    ids.sort_unstable();
-+    assert_eq!(ids, vec![60, 61, 62, 63]);
-+
-+    unsafe { lance_scanner_close(scanner) };
-+    unsafe { lance_dataset_close(ds) };
-+}
-+
-diff --git a/tests/cpp/test_cpp_api.cpp b/tests/cpp/test_cpp_api.cpp
-index 43491f2..444fbdc 100644
---- a/tests/cpp/test_cpp_api.cpp
-+++ b/tests/cpp/test_cpp_api.cpp
-@@ -124,0 +125,29 @@ static void test_dataset_take(const std::string& uri) {
-+static void test_dataset_take_rows(const std::string& uri) {
-+    TEST(test_dataset_take_rows);
-+
-+    auto ds = lance::Dataset::open(uri);
-+
-+    // The smoke fixture has one fragment, so its first row IDs are 0, 1, 2.
-+    uint64_t row_ids[] = {0, 1, 2};
-+    ArrowArrayStream stream;
-+    memset(&stream, 0, sizeof(stream));
-+    ds.take_rows(row_ids, 3, &stream);
-+
-+    uint64_t total = 0;
-+    while (true) {
-+        ArrowArray arr;
-+        memset(&arr, 0, sizeof(arr));
-+        int rc = stream.get_next(&stream, &arr);
-+        assert(rc == 0);
-+        if (!arr.release) break;
-+        total += (uint64_t)arr.length;
-+        arr.release(&arr);
-+    }
-+
-+    assert(total == 3);
-+    printf("rows=%llu... ", (unsigned long long)total);
-+
-+    if (stream.release) stream.release(&stream);
-+    PASS();
-+}
-+
-@@ -695,0 +725 @@ int main(int argc, char** argv) {
-+    test_dataset_take_rows(uri);
diff --git a/thirdparty/patches/lance-c-0.1.7-pr-64.patch 
b/thirdparty/patches/lance-c-0.1.7-pr-64.patch
new file mode 100644
index 00000000000..17f91bc7c31
--- /dev/null
+++ b/thirdparty/patches/lance-c-0.1.7-pr-64.patch
@@ -0,0 +1,1522 @@
+From e3320c1e7d5c72234b3b44e9e7e9a93a72fe488c Mon Sep 17 00:00:00 2001
+From: zhangstar333 <[email protected]>
+Date: Mon, 24 Aug 2026 16:37:33 +0800
+Subject: [PATCH 1/3] update
+
+---
+ include/lance/lance.h   |  73 ++++++++++++++++
+ include/lance/lance.hpp |  10 +++
+ src/scanner.rs          | 179 +++++++++++++++++++++++++++++++++++++-
+ tests/c_api_test.rs     | 188 +++++++++++++++++++++++++++++++++++++++-
+ 4 files changed, 448 insertions(+), 2 deletions(-)
+
+diff --git a/include/lance/lance.h b/include/lance/lance.h
+index 986905b..c6c3985 100644
+--- a/include/lance/lance.h
++++ b/include/lance/lance.h
+@@ -863,6 +863,79 @@ int32_t lance_scanner_set_substrait_filter(
+     size_t len
+ );
+ 
++/** Type of a dynamically named scan metric. */
++typedef enum {
++    LANCE_SCAN_METRIC_COUNT = 0,
++    LANCE_SCAN_METRIC_TIME_NANOSECONDS = 1,
++} LanceScanMetricKind;
++
++/**
++ * Borrowed view of one dynamically named scan metric.
++ *
++ * `name` is not NUL-terminated. `name` and this structure are valid only for
++ * the duration of the LanceScanStatisticsCallback invocation.
++ */
++typedef struct {
++    const char* name;
++    size_t name_len;
++    LanceScanMetricKind kind;
++    uint64_t value;
++} LanceScanMetric;
++
++/**
++ * Borrowed view of the execution statistics for one finalized scan.
++ *
++ * The fixed fields are stable summary metrics. `metrics` contains additional
++ * implementation-specific counters and timings. Those names are not a stable
++ * API and are intended for diagnostics and profiles. Dynamic metrics are
++ * best-effort and may be omitted if they cannot be materialized. `metrics` is
++ * NULL when `metrics_len` is zero.
++ */
++typedef struct {
++    uint64_t iops;
++    uint64_t requests;
++    uint64_t bytes_read;
++    uint64_t indices_loaded;
++    uint64_t index_partitions_loaded;
++    uint64_t index_comparisons;
++    const LanceScanMetric* metrics;
++    size_t metrics_len;
++} LanceScanStatistics;
++
++/**
++ * Receives scan statistics when a stream reaches EOF, fails, or is released.
++ *
++ * The statistics and all nested pointers are borrowed and valid only for the
++ * duration of this call. The callback may run on the thread that consumes or
++ * releases the scan stream and must therefore be thread-safe. It must return
++ * normally without throwing an exception or unwinding, and must not call any
++ * `lance_scanner_*` function with the originating scanner.
++ *
++ * Scan statistics are diagnostic and best-effort. The callback must handle 
its
++ * own errors and must not use them to abort or throw across this FFI 
boundary.
++ */
++typedef void (*LanceScanStatisticsCallback)(
++    void* callback_ctx,
++    const LanceScanStatistics* statistics
++);
++
++/**
++ * Register the execution-statistics callback for this scanner.
++ *
++ * Must be called before starting the scan; registering after the scan starts
++ * returns an error. `callback` must not be NULL. `callback_ctx` may be NULL. 
A
++ * non-NULL `callback_ctx` must remain valid, and `callback` must remain 
valid,
++ * until the stream reaches EOF, fails, or is released. Replaces a previously
++ * registered callback.
++ *
++ * @return 0 on success, -1 on error
++ */
++int32_t lance_scanner_set_statistics_callback(
++    LanceScanner* scanner,
++    LanceScanStatisticsCallback callback,
++    void* callback_ctx
++);
++
+ /** Close and free a scanner handle. */
+ void lance_scanner_close(LanceScanner* scanner);
+ 
+diff --git a/include/lance/lance.hpp b/include/lance/lance.hpp
+index afce358..9440a23 100644
+--- a/include/lance/lance.hpp
++++ b/include/lance/lance.hpp
+@@ -1127,6 +1127,16 @@ class Scanner {
+         return substrait_filter(bytes.data(), bytes.size());
+     }
+ 
++    /// Register a callback for scan execution statistics before starting the 
scan.
++    /// The callback may run on the thread that consumes or releases the 
stream. It
++    /// must be thread-safe, must not throw, and must not re-enter the 
originating
++    /// scanner. A non-null callback context must outlive the exported stream.
++    Scanner& statistics_callback(LanceScanStatisticsCallback callback, void* 
callback_ctx) {
++        if (lance_scanner_set_statistics_callback(handle_.get(), callback, 
callback_ctx) != 0)
++            check_error();
++        return *this;
++    }
++
+     /// Restrict the next k-NN query to a subset of vector index segments.
+     /// Pass `len` 16-byte UUIDs concatenated as a single byte buffer
+     /// (total bytes = `len * 16`). Pass len=0 (and any pointer) to clear.
+diff --git a/src/scanner.rs b/src/scanner.rs
+index f44b82f..d95089e 100644
+--- a/src/scanner.rs
++++ b/src/scanner.rs
+@@ -14,7 +14,9 @@ use arrow::ffi_stream::FFI_ArrowArrayStream;
+ use arrow_schema::SchemaRef;
+ use futures::{FutureExt, Stream, StreamExt};
+ use lance::Dataset;
+-use lance::dataset::scanner::DatasetRecordBatchStream;
++use lance::dataset::scanner::{
++    DatasetRecordBatchStream, ExecutionStatsCallback, ExecutionSummaryCounts,
++};
+ use lance_core::Result;
+ use lance_index::scalar::FullTextSearchQuery;
+ use lance_io::stream::RecordBatchStream;
+@@ -69,6 +71,8 @@ pub struct LanceScanner {
+     // the spawned async task can poison the handle from outside this call
+     // frame via `poison_flag()`.
+     poisoned: Arc<AtomicBool>,
++    scan_statistics_callback: Option<ExecutionStatsCallback>,
++    scan_started: AtomicBool,
+     // Materialized on first iteration call
+     stream: Option<Pin<Box<DatasetRecordBatchStream>>>,
+     #[allow(dead_code)]
+@@ -122,6 +126,8 @@ impl LanceScanner {
+             prefilter: false,
+             fts_query: None,
+             poisoned: Arc::new(AtomicBool::new(false)),
++            scan_statistics_callback: None,
++            scan_started: AtomicBool::new(false),
+             stream: None,
+             schema: None,
+         }
+@@ -157,6 +163,7 @@ impl LanceScanner {
+ 
+     /// Build the underlying Scanner and open a stream.
+     fn materialize_stream(&mut self) -> Result<()> {
++        self.scan_started.store(true, Ordering::Release);
+         let mut scanner = self.dataset.scan();
+         if let Some(cols) = &self.columns {
+             scanner.project(cols)?;
+@@ -212,6 +219,9 @@ impl LanceScanner {
+         if let Some(fts) = &self.fts_query {
+             scanner.full_text_search(fts.clone())?;
+         }
++        if let Some(callback) = &self.scan_statistics_callback {
++            scanner.scan_stats_callback(callback.clone());
++        }
+         let stream = block_on(scanner.try_into_stream())?;
+         self.schema = Some(stream.schema());
+         self.stream = Some(Box::pin(stream));
+@@ -220,6 +230,7 @@ impl LanceScanner {
+ 
+     /// Build a Scanner (without materializing) and return it.
+     fn build_scanner(&self) -> Result<lance::dataset::scanner::Scanner> {
++        self.scan_started.store(true, Ordering::Release);
+         let mut scanner = self.dataset.scan();
+         if let Some(cols) = &self.columns {
+             scanner.project(cols)?;
+@@ -274,10 +285,122 @@ impl LanceScanner {
+         if let Some(fts) = &self.fts_query {
+             scanner.full_text_search(fts.clone())?;
+         }
++        if let Some(callback) = &self.scan_statistics_callback {
++            scanner.scan_stats_callback(callback.clone());
++        }
+         Ok(scanner)
+     }
+ }
+ 
++/// Type of a dynamically named scan metric.
++#[repr(i32)]
++#[derive(Clone, Copy, Debug, PartialEq, Eq)]
++pub enum LanceScanMetricKind {
++    /// Monotonically accumulated counter.
++    Count = 0,
++    /// Accumulated duration in nanoseconds.
++    TimeNanoseconds = 1,
++}
++
++/// Borrowed view of one dynamically named scan metric.
++///
++/// `name` is not NUL-terminated. Both `name` and this structure are valid 
only
++/// for the duration of the scan statistics callback.
++#[repr(C)]
++#[derive(Clone, Copy, Debug)]
++pub struct LanceScanMetric {
++    pub name: *const c_char,
++    pub name_len: usize,
++    pub kind: LanceScanMetricKind,
++    pub value: u64,
++}
++
++/// Borrowed view of the execution statistics for one completed scan.
++///
++/// The fixed fields are stable summary metrics. `metrics` contains additional
++/// implementation-specific counters and timings and is valid only for the
++/// duration of the callback.
++#[repr(C)]
++#[derive(Clone, Copy, Debug)]
++pub struct LanceScanStatistics {
++    pub iops: u64,
++    pub requests: u64,
++    pub bytes_read: u64,
++    pub indices_loaded: u64,
++    pub index_partitions_loaded: u64,
++    pub index_comparisons: u64,
++    pub metrics: *const LanceScanMetric,
++    pub metrics_len: usize,
++}
++
++/// Callback invoked when a scan stream reaches EOF, fails, or is released.
++///
++/// The callback is an FFI boundary and must return normally without unwinding
++/// or throwing an exception. It must not call back into `lance_scanner_*` 
with
++/// the originating scanner.
++pub type LanceScanStatisticsCallback =
++    Option<unsafe extern "C" fn(ctx: *mut c_void, statistics: *const 
LanceScanStatistics)>;
++
++struct SendScanStatisticsCallback {
++    callback: unsafe extern "C" fn(*mut c_void, *const LanceScanStatistics),
++    ctx: *mut c_void,
++}
++
++// SAFETY: The C API requires the callback and its context to remain valid and
++// safe to invoke from the thread that consumes or releases the scan stream.
++unsafe impl Send for SendScanStatisticsCallback {}
++unsafe impl Sync for SendScanStatisticsCallback {}
++
++impl SendScanStatisticsCallback {
++    fn invoke(&self, counts: &ExecutionSummaryCounts) {
++        // Dynamic profile metrics are best-effort. Use fallible reservation 
so
++        // allocation failure omits them instead of aborting the embedding 
process.
++        let mut metrics = Vec::new();
++        if let Some(metrics_len) = 
counts.all_counts.len().checked_add(counts.all_times.len())
++            && metrics.try_reserve_exact(metrics_len).is_ok()
++        {
++            metrics.extend(
++                counts
++                    .all_counts
++                    .iter()
++                    .map(|(name, value)| LanceScanMetric {
++                        name: name.as_ptr().cast(),
++                        name_len: name.len(),
++                        kind: LanceScanMetricKind::Count,
++                        value: *value as u64,
++                    }),
++            );
++            metrics.extend(
++                counts
++                    .all_times
++                    .iter()
++                    .map(|(name, value)| LanceScanMetric {
++                        name: name.as_ptr().cast(),
++                        name_len: name.len(),
++                        kind: LanceScanMetricKind::TimeNanoseconds,
++                        value: *value as u64,
++                    }),
++            );
++        }
++
++        let statistics = LanceScanStatistics {
++            iops: counts.iops as u64,
++            requests: counts.requests as u64,
++            bytes_read: counts.bytes_read as u64,
++            indices_loaded: counts.indices_loaded as u64,
++            index_partitions_loaded: counts.parts_loaded as u64,
++            index_comparisons: counts.index_comparisons as u64,
++            metrics: if metrics.is_empty() {
++                ptr::null()
++            } else {
++                metrics.as_ptr()
++            },
++            metrics_len: metrics.len(),
++        };
++        unsafe { (self.callback)(self.ctx, &statistics) };
++    }
++}
++
+ // ---------------------------------------------------------------------------
+ // Poison check shared by all `lance_scanner_*` entry points
+ // ---------------------------------------------------------------------------
+@@ -529,6 +652,60 @@ unsafe fn scanner_set_substrait_filter_inner(
+     Ok(0)
+ }
+ 
++/// Register a callback that receives execution statistics when the scan 
stream
++/// reaches EOF, fails, or is released.
++///
++/// The callback and `callback_ctx` must remain valid until the scan stream is
++/// finalized. Metric names and arrays passed to the callback are borrowed and
++/// must be copied if the caller needs to retain them. The callback must be
++/// thread-safe, must return normally without unwinding or throwing an
++/// exception, and must not call `lance_scanner_*` with the originating 
scanner.
++#[unsafe(no_mangle)]
++pub unsafe extern "C" fn lance_scanner_set_statistics_callback(
++    scanner: *mut LanceScanner,
++    callback: LanceScanStatisticsCallback,
++    callback_ctx: *mut c_void,
++) -> i32 {
++    scanner_poison_check!(scanner, -1);
++    ffi_try!(
++        unsafe { scanner_set_statistics_callback_inner(scanner, callback, 
callback_ctx) },
++        neg
++    )
++}
++
++unsafe fn scanner_set_statistics_callback_inner(
++    scanner: *mut LanceScanner,
++    callback: LanceScanStatisticsCallback,
++    callback_ctx: *mut c_void,
++) -> Result<i32> {
++    if scanner.is_null() {
++        return Err(lance_core::Error::invalid_input_source(
++            "scanner is NULL".into(),
++        ));
++    }
++    let Some(callback) = callback else {
++        return Err(lance_core::Error::invalid_input_source(
++            "statistics callback is NULL".into(),
++        ));
++    };
++
++    let s = unsafe { &mut *scanner };
++    if s.scan_started.load(Ordering::Acquire) {
++        return Err(lance_core::Error::invalid_input_source(
++            "statistics callback must be registered before the scan 
starts".into(),
++        ));
++    }
++
++    let callback = SendScanStatisticsCallback {
++        callback,
++        ctx: callback_ctx,
++    };
++    s.scan_statistics_callback = Some(Arc::new(move |counts: 
&ExecutionSummaryCounts| {
++        callback.invoke(counts);
++    }));
++    Ok(0)
++}
++
+ /// Close and free a scanner handle.
+ ///
+ /// Best-effort (issue #61): this drops a possibly-live
+diff --git a/tests/c_api_test.rs b/tests/c_api_test.rs
+index d6cd928..db35fea 100644
+--- a/tests/c_api_test.rs
++++ b/tests/c_api_test.rs
+@@ -6,7 +6,7 @@
+ //! These tests call the `extern "C"` functions directly from Rust,
+ //! validating the C API contract without needing a C compiler.
+ 
+-use std::ffi::{CString, c_char};
++use std::ffi::{CString, c_char, c_void};
+ use std::process::Command;
+ use std::ptr;
+ use std::sync::Arc;
+@@ -99,6 +99,58 @@ fn c_str(s: &str) -> CString {
+     CString::new(s).unwrap()
+ }
+ 
++#[derive(Default)]
++struct CapturedScanStatistics {
++    calls: usize,
++    iops: u64,
++    requests: u64,
++    bytes_read: u64,
++    indices_loaded: u64,
++    index_partitions_loaded: u64,
++    index_comparisons: u64,
++    metrics: Vec<(String, LanceScanMetricKind, u64)>,
++}
++
++unsafe extern "C" fn capture_scan_statistics(
++    callback_ctx: *mut c_void,
++    statistics: *const LanceScanStatistics,
++) {
++    assert!(!callback_ctx.is_null());
++    assert!(!statistics.is_null());
++    let captured = unsafe { &mut 
*callback_ctx.cast::<CapturedScanStatistics>() };
++    let statistics = unsafe { &*statistics };
++    let metrics = if statistics.metrics_len == 0 {
++        &[]
++    } else {
++        assert!(!statistics.metrics.is_null());
++        unsafe { std::slice::from_raw_parts(statistics.metrics, 
statistics.metrics_len) }
++    };
++
++    captured.calls += 1;
++    captured.iops = statistics.iops;
++    captured.requests = statistics.requests;
++    captured.bytes_read = statistics.bytes_read;
++    captured.indices_loaded = statistics.indices_loaded;
++    captured.index_partitions_loaded = statistics.index_partitions_loaded;
++    captured.index_comparisons = statistics.index_comparisons;
++    captured.metrics = metrics
++        .iter()
++        .map(|metric| {
++            let name = if metric.name_len == 0 {
++                &[]
++            } else {
++                assert!(!metric.name.is_null());
++                unsafe { std::slice::from_raw_parts(metric.name.cast::<u8>(), 
metric.name_len) }
++            };
++            (
++                std::str::from_utf8(name).unwrap().to_owned(),
++                metric.kind,
++                metric.value,
++            )
++        })
++        .collect();
++}
++
+ /// Helper: build a tiny dataset whose `value` column is nullable AND contains
+ /// at least one NULL. Used by tests that need to exercise upstream's
+ /// nullability-tightening pre-scan failure path.
+@@ -284,6 +336,140 @@ fn test_scanner_to_arrow_stream() {
+     unsafe { lance_dataset_close(ds) };
+ }
+ 
++#[test]
++fn test_scanner_statistics_callback_with_next_multi_fragment() {
++    let (_tmp, uri) = create_multi_fragment_dataset();
++    let c_uri = c_str(&uri);
++    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
++    assert!(!ds.is_null());
++    assert_eq!(unsafe { lance_dataset_fragment_count(ds) }, 2);
++
++    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), ptr::null()) };
++    assert!(!scanner.is_null());
++    let mut captured = CapturedScanStatistics::default();
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics),
++                (&mut captured as *mut CapturedScanStatistics).cast(),
++            )
++        },
++        0
++    );
++
++    loop {
++        let mut batch = ptr::null_mut();
++        match unsafe { lance_scanner_next(scanner, &mut batch) } {
++            0 => unsafe { lance_batch_free(batch) },
++            1 => break,
++            status => panic!("scanner_next returned error: {status}"),
++        }
++    }
++
++    assert_eq!(captured.calls, 1);
++    assert!(captured.bytes_read > 0);
++    assert!(captured.requests > 0);
++    assert!(captured.metrics.iter().all(|(name, _, _)| !name.is_empty()));
++
++    unsafe { lance_scanner_close(scanner) };
++    unsafe { lance_dataset_close(ds) };
++}
++
++#[test]
++fn test_scanner_statistics_callback_with_arrow_stream() {
++    let (_tmp, uri) = create_test_dataset();
++    let c_uri = c_str(&uri);
++    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
++    assert!(!ds.is_null());
++
++    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), ptr::null()) };
++    assert!(!scanner.is_null());
++    let mut captured = CapturedScanStatistics::default();
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics),
++                (&mut captured as *mut CapturedScanStatistics).cast(),
++            )
++        },
++        0
++    );
++
++    let mut stream = FFI_ArrowArrayStream::empty();
++    assert_eq!(unsafe { lance_scanner_to_arrow_stream(scanner, &mut stream) 
}, 0);
++    let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut stream) 
}.unwrap();
++    assert_eq!(reader.map(|batch| batch.unwrap().num_rows()).sum::<usize>(), 
5);
++    assert_eq!(captured.calls, 1);
++    assert!(captured.bytes_read > 0);
++
++    unsafe { lance_scanner_close(scanner) };
++    unsafe { lance_dataset_close(ds) };
++}
++
++#[test]
++fn test_scanner_statistics_callback_rejects_null_inputs() {
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                ptr::null_mut(),
++                Some(capture_scan_statistics),
++                ptr::null_mut(),
++            )
++        },
++        -1
++    );
++    assert_eq!(lance_last_error_code(), LanceErrorCode::InvalidArgument);
++
++    let (_tmp, uri) = create_test_dataset();
++    let c_uri = c_str(&uri);
++    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
++    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), ptr::null()) };
++    assert_eq!(
++        unsafe { lance_scanner_set_statistics_callback(scanner, None, 
ptr::null_mut()) },
++        -1
++    );
++    assert_eq!(lance_last_error_code(), LanceErrorCode::InvalidArgument);
++
++    unsafe { lance_scanner_close(scanner) };
++    unsafe { lance_dataset_close(ds) };
++}
++
++#[test]
++fn test_scanner_statistics_callback_rejects_registration_after_scan_started() 
{
++    let (_tmp, uri) = create_test_dataset();
++    let c_uri = c_str(&uri);
++    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
++    assert!(!ds.is_null());
++    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), ptr::null()) };
++    assert!(!scanner.is_null());
++
++    let mut batch = ptr::null_mut();
++    assert_eq!(unsafe { lance_scanner_next(scanner, &mut batch) }, 0);
++    assert!(!batch.is_null());
++    unsafe { lance_batch_free(batch) };
++
++    let mut captured = CapturedScanStatistics::default();
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics),
++                (&mut captured as *mut CapturedScanStatistics).cast(),
++            )
++        },
++        -1
++    );
++    assert_eq!(lance_last_error_code(), LanceErrorCode::InvalidArgument);
++    let error = take_last_error_message();
++    assert!(error.contains("before the scan starts"), "{error}");
++
++    unsafe { lance_scanner_close(scanner) };
++    assert_eq!(captured.calls, 0);
++    unsafe { lance_dataset_close(ds) };
++}
++
+ #[test]
+ fn test_scanner_with_filter() {
+     let (_tmp, uri) = create_test_dataset();
+
+From 8e6e92140031c06184da079b8f6c194799c0de88 Mon Sep 17 00:00:00 2001
+From: zhangstar333 <[email protected]>
+Date: Mon, 24 Aug 2026 17:04:51 +0800
+Subject: [PATCH 2/3] formatter
+
+---
+ include/lance/lance.h   |  23 ++++---
+ include/lance/lance.hpp |   9 +--
+ src/scanner.rs          |  23 ++++---
+ tests/c_api_test.rs     | 134 +++++++++++++++++++++++++++++++++++++++-
+ 4 files changed, 165 insertions(+), 24 deletions(-)
+
+diff --git a/include/lance/lance.h b/include/lance/lance.h
+index c6c3985..1a6822b 100644
+--- a/include/lance/lance.h
++++ b/include/lance/lance.h
+@@ -883,7 +883,7 @@ typedef struct {
+ } LanceScanMetric;
+ 
+ /**
+- * Borrowed view of the execution statistics for one finalized scan.
++ * Borrowed view of the execution statistics for one fully consumed scan.
+  *
+  * The fixed fields are stable summary metrics. `metrics` contains additional
+  * implementation-specific counters and timings. Those names are not a stable
+@@ -903,13 +903,13 @@ typedef struct {
+ } LanceScanStatistics;
+ 
+ /**
+- * Receives scan statistics when a stream reaches EOF, fails, or is released.
++ * Receives scan statistics after a stream is fully consumed to EOF.
+  *
+  * The statistics and all nested pointers are borrowed and valid only for the
+- * duration of this call. The callback may run on the thread that consumes or
+- * releases the scan stream and must therefore be thread-safe. It must return
+- * normally without throwing an exception or unwinding, and must not call any
+- * `lance_scanner_*` function with the originating scanner.
++ * duration of this call. The callback may run on the thread that observes EOF
++ * and must therefore be thread-safe. It must return normally without throwing
++ * an exception or unwinding, and must not call any `lance_scanner_*` function
++ * with the originating scanner.
+  *
+  * Scan statistics are diagnostic and best-effort. The callback must handle 
its
+  * own errors and must not use them to abort or throw across this FFI 
boundary.
+@@ -925,8 +925,15 @@ typedef void (*LanceScanStatisticsCallback)(
+  * Must be called before starting the scan; registering after the scan starts
+  * returns an error. `callback` must not be NULL. `callback_ctx` may be NULL. 
A
+  * non-NULL `callback_ctx` must remain valid, and `callback` must remain 
valid,
+- * until the stream reaches EOF, fails, or is released. Replaces a previously
+- * registered callback.
++ * until the callback returns or, if the callback has not run, until the 
owning
++ * scan stream is released. For `lance_scanner_next` and
++ * `lance_scanner_poll_next`, the scanner owns the stream. For an exported
++ * ArrowArrayStream, the Arrow stream owns it independently of the scanner.
++ *
++ * The callback is invoked exactly once when the stream is fully consumed to
++ * EOF. It is not guaranteed to run if execution fails, the scan is cancelled,
++ * or the scanner / ArrowArrayStream is released before EOF. Replaces a
++ * previously registered callback.
+  *
+  * @return 0 on success, -1 on error
+  */
+diff --git a/include/lance/lance.hpp b/include/lance/lance.hpp
+index 9440a23..4268801 100644
+--- a/include/lance/lance.hpp
++++ b/include/lance/lance.hpp
+@@ -1127,10 +1127,11 @@ class Scanner {
+         return substrait_filter(bytes.data(), bytes.size());
+     }
+ 
+-    /// Register a callback for scan execution statistics before starting the 
scan.
+-    /// The callback may run on the thread that consumes or releases the 
stream. It
+-    /// must be thread-safe, must not throw, and must not re-enter the 
originating
+-    /// scanner. A non-null callback context must outlive the exported stream.
++    /// Register a callback for scan statistics after successful full 
exhaustion.
++    /// The callback is not guaranteed on error, cancellation, or early 
release. It
++    /// may run on the thread that observes EOF, must be thread-safe, must 
not throw,
++    /// and must not re-enter the originating scanner. The callback and a 
non-null
++    /// context must remain valid until the callback returns or the stream is 
released.
+     Scanner& statistics_callback(LanceScanStatisticsCallback callback, void* 
callback_ctx) {
+         if (lance_scanner_set_statistics_callback(handle_.get(), callback, 
callback_ctx) != 0)
+             check_error();
+diff --git a/src/scanner.rs b/src/scanner.rs
+index d95089e..414c269 100644
+--- a/src/scanner.rs
++++ b/src/scanner.rs
+@@ -315,7 +315,7 @@ pub struct LanceScanMetric {
+     pub value: u64,
+ }
+ 
+-/// Borrowed view of the execution statistics for one completed scan.
++/// Borrowed view of the execution statistics for one fully consumed scan.
+ ///
+ /// The fixed fields are stable summary metrics. `metrics` contains additional
+ /// implementation-specific counters and timings and is valid only for the
+@@ -333,7 +333,7 @@ pub struct LanceScanStatistics {
+     pub metrics_len: usize,
+ }
+ 
+-/// Callback invoked when a scan stream reaches EOF, fails, or is released.
++/// Callback invoked after a scan stream is fully consumed to EOF.
+ ///
+ /// The callback is an FFI boundary and must return normally without unwinding
+ /// or throwing an exception. It must not call back into `lance_scanner_*` 
with
+@@ -347,7 +347,7 @@ struct SendScanStatisticsCallback {
+ }
+ 
+ // SAFETY: The C API requires the callback and its context to remain valid and
+-// safe to invoke from the thread that consumes or releases the scan stream.
++// safe to invoke from the thread that observes the scan stream's EOF.
+ unsafe impl Send for SendScanStatisticsCallback {}
+ unsafe impl Sync for SendScanStatisticsCallback {}
+ 
+@@ -652,14 +652,17 @@ unsafe fn scanner_set_substrait_filter_inner(
+     Ok(0)
+ }
+ 
+-/// Register a callback that receives execution statistics when the scan 
stream
+-/// reaches EOF, fails, or is released.
++/// Register a callback that receives execution statistics after the scan 
stream
++/// is fully consumed to EOF.
+ ///
+-/// The callback and `callback_ctx` must remain valid until the scan stream is
+-/// finalized. Metric names and arrays passed to the callback are borrowed and
+-/// must be copied if the caller needs to retain them. The callback must be
+-/// thread-safe, must return normally without unwinding or throwing an
+-/// exception, and must not call `lance_scanner_*` with the originating 
scanner.
++/// The callback is not guaranteed to run if execution fails, the scan is
++/// cancelled, or the scanner / exported Arrow stream is released before EOF.
++/// The callback and `callback_ctx` must remain valid until the callback 
returns
++/// or, if it has not run, until the owning scan stream is released. Metric 
names
++/// and arrays passed to the callback are borrowed and must be copied if the
++/// caller needs to retain them. The callback must be thread-safe, must return
++/// normally without unwinding or throwing an exception, and must not call
++/// `lance_scanner_*` with the originating scanner.
+ #[unsafe(no_mangle)]
+ pub unsafe extern "C" fn lance_scanner_set_statistics_callback(
+     scanner: *mut LanceScanner,
+diff --git a/tests/c_api_test.rs b/tests/c_api_test.rs
+index db35fea..c1bb71d 100644
+--- a/tests/c_api_test.rs
++++ b/tests/c_api_test.rs
+@@ -372,7 +372,43 @@ fn 
test_scanner_statistics_callback_with_next_multi_fragment() {
+     assert!(captured.requests > 0);
+     assert!(captured.metrics.iter().all(|(name, _, _)| !name.is_empty()));
+ 
++    let mut batch = ptr::null_mut();
++    assert_eq!(unsafe { lance_scanner_next(scanner, &mut batch) }, 1);
++    assert!(batch.is_null());
++    assert_eq!(captured.calls, 1, "callback must run exactly once");
++
++    unsafe { lance_scanner_close(scanner) };
++    unsafe { lance_dataset_close(ds) };
++}
++
++#[test]
++fn test_scanner_statistics_callback_not_called_on_early_scanner_close() {
++    let (_tmp, uri) = create_test_dataset();
++    let c_uri = c_str(&uri);
++    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
++    assert!(!ds.is_null());
++
++    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), ptr::null()) };
++    assert!(!scanner.is_null());
++    let mut captured = CapturedScanStatistics::default();
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics),
++                (&mut captured as *mut CapturedScanStatistics).cast(),
++            )
++        },
++        0
++    );
++
++    let mut batch = ptr::null_mut();
++    assert_eq!(unsafe { lance_scanner_next(scanner, &mut batch) }, 0);
++    assert!(!batch.is_null());
++    unsafe { lance_batch_free(batch) };
++
+     unsafe { lance_scanner_close(scanner) };
++    assert_eq!(captured.calls, 0);
+     unsafe { lance_dataset_close(ds) };
+ }
+ 
+@@ -398,9 +434,15 @@ fn test_scanner_statistics_callback_with_arrow_stream() {
+     );
+ 
+     let mut stream = FFI_ArrowArrayStream::empty();
+-    assert_eq!(unsafe { lance_scanner_to_arrow_stream(scanner, &mut stream) 
}, 0);
++    assert_eq!(
++        unsafe { lance_scanner_to_arrow_stream(scanner, &mut stream) },
++        0
++    );
+     let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut stream) 
}.unwrap();
+-    assert_eq!(reader.map(|batch| batch.unwrap().num_rows()).sum::<usize>(), 
5);
++    assert_eq!(
++        reader.map(|batch| batch.unwrap().num_rows()).sum::<usize>(),
++        5
++    );
+     assert_eq!(captured.calls, 1);
+     assert!(captured.bytes_read > 0);
+ 
+@@ -408,6 +450,74 @@ fn test_scanner_statistics_callback_with_arrow_stream() {
+     unsafe { lance_dataset_close(ds) };
+ }
+ 
++#[test]
++fn 
test_scanner_statistics_callback_not_called_on_early_arrow_stream_release() {
++    let (_tmp, uri) = create_test_dataset();
++    let c_uri = c_str(&uri);
++    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
++    assert!(!ds.is_null());
++
++    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), ptr::null()) };
++    assert!(!scanner.is_null());
++    let mut captured = CapturedScanStatistics::default();
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics),
++                (&mut captured as *mut CapturedScanStatistics).cast(),
++            )
++        },
++        0
++    );
++
++    let mut stream = FFI_ArrowArrayStream::empty();
++    assert_eq!(
++        unsafe { lance_scanner_to_arrow_stream(scanner, &mut stream) },
++        0
++    );
++    let mut reader = unsafe { ArrowArrayStreamReader::from_raw(&mut stream) 
}.unwrap();
++    assert!(reader.next().unwrap().is_ok());
++    drop(reader);
++
++    assert_eq!(captured.calls, 0);
++    unsafe { lance_scanner_close(scanner) };
++    assert_eq!(captured.calls, 0);
++    unsafe { lance_dataset_close(ds) };
++}
++
++#[test]
++fn test_scanner_statistics_callback_not_called_on_materialization_error() {
++    let (_tmp, uri) = create_test_dataset();
++    let c_uri = c_str(&uri);
++    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
++    assert!(!ds.is_null());
++
++    let bad_filter = c_str("NOT A VALID >>> FILTER ???");
++    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), 
bad_filter.as_ptr()) };
++    assert!(!scanner.is_null());
++    let mut captured = CapturedScanStatistics::default();
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics),
++                (&mut captured as *mut CapturedScanStatistics).cast(),
++            )
++        },
++        0
++    );
++
++    let mut batch = ptr::null_mut();
++    assert_eq!(unsafe { lance_scanner_next(scanner, &mut batch) }, -1);
++    assert!(batch.is_null());
++    assert_eq!(captured.calls, 0);
++
++    unsafe { lance_scanner_close(scanner) };
++    assert_eq!(captured.calls, 0);
++    unsafe { lance_dataset_close(ds) };
++}
++
+ #[test]
+ fn test_scanner_statistics_callback_rejects_null_inputs() {
+     assert_eq!(
+@@ -1367,6 +1477,17 @@ fn test_poll_next_basic() {
+         let c_uri = c_str(&uri_clone);
+         let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) 
};
+         let scanner = unsafe { lance_scanner_new(ds, ptr::null(), 
ptr::null()) };
++        let mut captured = CapturedScanStatistics::default();
++        assert_eq!(
++            unsafe {
++                lance_scanner_set_statistics_callback(
++                    scanner,
++                    Some(capture_scan_statistics),
++                    (&mut captured as *mut CapturedScanStatistics).cast(),
++                )
++            },
++            0
++        );
+ 
+         use std::sync::atomic::{AtomicBool, Ordering};
+         static WOKE: AtomicBool = AtomicBool::new(false);
+@@ -1401,6 +1522,15 @@ fn test_poll_next_basic() {
+             assert!(iterations < 1000, "poll loop should not spin forever");
+         }
+         assert_eq!(total_rows, 5);
++        assert_eq!(captured.calls, 1);
++
++        let mut batch: *mut LanceBatch = ptr::null_mut();
++        assert_eq!(
++            unsafe { lance_scanner_poll_next(scanner, test_waker, 
ptr::null_mut(), &mut batch) },
++            LancePollStatus::Finished
++        );
++        assert!(batch.is_null());
++        assert_eq!(captured.calls, 1, "callback must run exactly once");
+ 
+         unsafe { lance_scanner_close(scanner) };
+         unsafe { lance_dataset_close(ds) };
+
+From fa168ef99951d0396e50c060e540dee93e14e2be Mon Sep 17 00:00:00 2001
+From: zhangstar333 <[email protected]>
+Date: Mon, 24 Aug 2026 20:47:41 +0800
+Subject: [PATCH 3/3] update
+
+---
+ include/lance/lance.h      |  55 ++++++++---
+ include/lance/lance.hpp    |  22 ++++-
+ src/scanner.rs             |  51 +++++++---
+ tests/c_api_test.rs        | 188 +++++++++++++++++++++++++++++++------
+ tests/cpp/test_c_api.c     |  39 +++++++-
+ tests/cpp/test_cpp_api.cpp |  28 +++++-
+ 6 files changed, 319 insertions(+), 64 deletions(-)
+
+diff --git a/include/lance/lance.h b/include/lance/lance.h
+index 1a6822b..5b12f3d 100644
+--- a/include/lance/lance.h
++++ b/include/lance/lance.h
+@@ -873,7 +873,8 @@ typedef enum {
+  * Borrowed view of one dynamically named scan metric.
+  *
+  * `name` is not NUL-terminated. `name` and this structure are valid only for
+- * the duration of the LanceScanStatisticsCallback invocation.
++ * the duration of the LanceScanStatisticsCallback invocation. Metric order is
++ * unspecified.
+  */
+ typedef struct {
+     const char* name;
+@@ -905,11 +906,22 @@ typedef struct {
+ /**
+  * Receives scan statistics after a stream is fully consumed to EOF.
+  *
+- * The statistics and all nested pointers are borrowed and valid only for the
+- * duration of this call. The callback may run on the thread that observes EOF
+- * and must therefore be thread-safe. It must return normally without throwing
+- * an exception or unwinding, and must not call any `lance_scanner_*` function
+- * with the originating scanner.
++ * `statistics` is non-NULL. It and all nested pointers are borrowed and valid
++ * only for the duration of this call. The callback may run on the thread that
++ * observes EOF and must therefore be thread-safe. It must return normally
++ * without throwing an exception or unwinding, and must not call any
++ * `lance_scanner_*` function with the originating scanner.
++ *
++ * From callback entry until the enclosing operation that observes EOF has
++ * returned to its caller, the callback must not directly or indirectly cause
++ * `get_schema`, `get_next`, `get_last_error`, or `release` to be called on 
any
++ * ArrowArrayStream derived from the originating scanner, nor cause such a
++ * stream to be moved, destroyed, or otherwise accessed. This includes 
signaling
++ * or scheduling another thread to act based only on callback completion: the
++ * callback returns before the enclosing stream operation does. Such 
interaction
++ * is reentrant and has undefined behavior. Normal access may resume only 
after
++ * the enclosing ArrowArrayStream `get_next`, `lance_scanner_next`, or
++ * `lance_scanner_poll_next` call returns to its caller.
+  *
+  * Scan statistics are diagnostic and best-effort. The callback must handle 
its
+  * own errors and must not use them to abort or throw across this FFI 
boundary.
+@@ -925,15 +937,27 @@ typedef void (*LanceScanStatisticsCallback)(
+  * Must be called before starting the scan; registering after the scan starts
+  * returns an error. `callback` must not be NULL. `callback_ctx` may be NULL. 
A
+  * non-NULL `callback_ctx` must remain valid, and `callback` must remain 
valid,
+- * until the callback returns or, if the callback has not run, until the 
owning
+- * scan stream is released. For `lance_scanner_next` and
+- * `lance_scanner_poll_next`, the scanner owns the stream. For an exported
+- * ArrowArrayStream, the Arrow stream owns it independently of the scanner.
+- *
+- * The callback is invoked exactly once when the stream is fully consumed to
+- * EOF. It is not guaranteed to run if execution fails, the scan is cancelled,
+- * or the scanner / ArrowArrayStream is released before EOF. Replaces a
+- * previously registered callback.
++ * until all of the following are true: the scanner is closed, every in-flight
++ * `lance_scanner_scan_async` call has delivered its completion callback, and
++ * every ArrowArrayStream derived from the scanner has been released. The
++ * registration remains installed after a callback returns and applies to
++ * streams created later from the same scanner. For `lance_scanner_next` and
++ * `lance_scanner_poll_next`, the scanner owns the stream. Exported and
++ * asynchronous ArrowArrayStreams own their registrations independently of the
++ * scanner and may invoke the callback after the scanner is closed. Concurrent
++ * streams may invoke the callback concurrently.
++ *
++ * The callback is invoked exactly once for each derived stream that is fully
++ * consumed to EOF. It is not guaranteed to run for a stream if execution 
fails,
++ * the scan is cancelled, or the scanner / ArrowArrayStream is released before
++ * EOF. Before scanning starts, a new registration replaces the previous one;
++ * after a successful replacement, the previous callback and context are no
++ * longer retained and may be retired.
++ *
++ * From callback entry until the enclosing EOF-observing operation returns, 
the
++ * callback must not directly or indirectly cause interaction with any
++ * ArrowArrayStream derived from this scanner; see LanceScanStatisticsCallback
++ * for the complete reentrancy restriction.
+  *
+  * @return 0 on success, -1 on error
+  */
+@@ -950,6 +974,7 @@ void lance_scanner_close(LanceScanner* scanner);
+ 
+ /**
+  * Materialize the scan as an ArrowArrayStream (blocking).
++ * The scanner remains valid, and each call creates an independent stream.
+  *
+  * Reading the exported stream may surface a mid-iteration panic as one
+  * error through the Arrow C stream contract (nonzero get_next plus
+diff --git a/include/lance/lance.hpp b/include/lance/lance.hpp
+index 4268801..8aa97e2 100644
+--- a/include/lance/lance.hpp
++++ b/include/lance/lance.hpp
+@@ -1128,10 +1128,22 @@ class Scanner {
+     }
+ 
+     /// Register a callback for scan statistics after successful full 
exhaustion.
+-    /// The callback is not guaranteed on error, cancellation, or early 
release. It
+-    /// may run on the thread that observes EOF, must be thread-safe, must 
not throw,
+-    /// and must not re-enter the originating scanner. The callback and a 
non-null
+-    /// context must remain valid until the callback returns or the stream is 
released.
++    /// The registration applies to every stream derived from this scanner, 
including
++    /// concurrent streams and streams created after an earlier callback 
returns. The
++    /// callback is not guaranteed on error, cancellation, or early release. 
It may
++    /// run on the thread that observes EOF, must be thread-safe, must not 
throw, and
++    /// must not re-enter the originating scanner. The callback and a 
non-null context
++    /// must remain valid until the scanner is closed, all async scan 
requests have
++    /// delivered their completion callbacks, and all derived streams are 
released.
++    /// From callback entry until the enclosing operation that observes EOF 
has
++    /// returned to its caller, the callback must not directly or indirectly 
cause
++    /// any ArrowArrayStream derived from this Scanner to be accessed, called,
++    /// released, moved, or destroyed. This includes signaling or scheduling 
another
++    /// thread to act based only on callback completion: the callback returns 
before
++    /// the enclosing stream operation does. Such interaction is reentrant 
and has
++    /// undefined behavior. Normal access may resume only after the enclosing
++    /// ArrowArrayStream `get_next`, `lance_scanner_next`, or
++    /// `lance_scanner_poll_next` call returns.
+     Scanner& statistics_callback(LanceScanStatisticsCallback callback, void* 
callback_ctx) {
+         if (lance_scanner_set_statistics_callback(handle_.get(), callback, 
callback_ctx) != 0)
+             check_error();
+@@ -1153,7 +1165,7 @@ class Scanner {
+         return index_segments(reinterpret_cast<const uint8_t*>(uuids.data()), 
uuids.size());
+     }
+ 
+-    /// Materialize the scan as an ArrowArrayStream (blocking).
++    /// Materialize an independent ArrowArrayStream (blocking). The scanner 
remains valid.
+     void to_arrow_stream(ArrowArrayStream* out) {
+         if (lance_scanner_to_arrow_stream(handle_.get(), out) != 0)
+             check_error();
+diff --git a/src/scanner.rs b/src/scanner.rs
+index 414c269..ef9d290 100644
+--- a/src/scanner.rs
++++ b/src/scanner.rs
+@@ -305,7 +305,7 @@ pub enum LanceScanMetricKind {
+ /// Borrowed view of one dynamically named scan metric.
+ ///
+ /// `name` is not NUL-terminated. Both `name` and this structure are valid 
only
+-/// for the duration of the scan statistics callback.
++/// for the duration of the scan statistics callback. Metric order is 
unspecified.
+ #[repr(C)]
+ #[derive(Clone, Copy, Debug)]
+ pub struct LanceScanMetric {
+@@ -318,8 +318,10 @@ pub struct LanceScanMetric {
+ /// Borrowed view of the execution statistics for one fully consumed scan.
+ ///
+ /// The fixed fields are stable summary metrics. `metrics` contains additional
+-/// implementation-specific counters and timings and is valid only for the
+-/// duration of the callback.
++/// implementation-specific counters and timings whose names are not a stable 
API
++/// and are intended only for diagnostics and profiles. Dynamic metrics are
++/// best-effort and may be omitted if they cannot be materialized. `metrics` 
is
++/// null when `metrics_len` is zero and is valid only for the callback 
duration.
+ #[repr(C)]
+ #[derive(Clone, Copy, Debug)]
+ pub struct LanceScanStatistics {
+@@ -333,13 +335,20 @@ pub struct LanceScanStatistics {
+     pub metrics_len: usize,
+ }
+ 
+-/// Callback invoked after a scan stream is fully consumed to EOF.
++/// Callback invoked once for each derived scan stream that is fully consumed 
to EOF.
+ ///
+ /// The callback is an FFI boundary and must return normally without unwinding
+ /// or throwing an exception. It must not call back into `lance_scanner_*` 
with
+-/// the originating scanner.
++/// the originating scanner. From callback entry until the enclosing operation
++/// that observes EOF has returned to its caller, the callback must not 
directly
++/// or indirectly cause any Arrow C stream derived from the originating 
scanner to
++/// be called, released, moved, destroyed, or otherwise accessed. This 
includes
++/// signaling or scheduling another thread to act based only on callback 
completion:
++/// the callback returns before the enclosing stream operation does. Such 
interaction
++/// is reentrant and has undefined behavior. Normal access may resume only 
after the
++/// enclosing `get_next`, `lance_scanner_next`, or `lance_scanner_poll_next` 
returns.
+ pub type LanceScanStatisticsCallback =
+-    Option<unsafe extern "C" fn(ctx: *mut c_void, statistics: *const 
LanceScanStatistics)>;
++    Option<unsafe extern "C" fn(callback_ctx: *mut c_void, statistics: *const 
LanceScanStatistics)>;
+ 
+ struct SendScanStatisticsCallback {
+     callback: unsafe extern "C" fn(*mut c_void, *const LanceScanStatistics),
+@@ -347,7 +356,9 @@ struct SendScanStatisticsCallback {
+ }
+ 
+ // SAFETY: The C API requires the callback and its context to remain valid and
+-// safe to invoke from the thread that observes the scan stream's EOF.
++// safe to invoke until the scanner is closed, every in-flight asynchronous 
scan
++// has delivered its completion callback, and every derived stream has been
++// released. Concurrent derived streams may invoke the callback concurrently.
+ unsafe impl Send for SendScanStatisticsCallback {}
+ unsafe impl Sync for SendScanStatisticsCallback {}
+ 
+@@ -657,12 +668,24 @@ unsafe fn scanner_set_substrait_filter_inner(
+ ///
+ /// The callback is not guaranteed to run if execution fails, the scan is
+ /// cancelled, or the scanner / exported Arrow stream is released before EOF.
+-/// The callback and `callback_ctx` must remain valid until the callback 
returns
+-/// or, if it has not run, until the owning scan stream is released. Metric 
names
+-/// and arrays passed to the callback are borrowed and must be copied if the
+-/// caller needs to retain them. The callback must be thread-safe, must return
+-/// normally without unwinding or throwing an exception, and must not call
+-/// `lance_scanner_*` with the originating scanner.
++/// The registration applies to every stream derived from this scanner, 
including
++/// streams created after an earlier callback has returned. The callback and
++/// `callback_ctx` must remain valid until the scanner is closed, every 
in-flight
++/// asynchronous scan has delivered its completion callback, and every derived
++/// stream has been released.
++/// Metric names and arrays passed to the callback are borrowed and must be 
copied
++/// if the caller needs to retain them. The callback must be thread-safe, must
++/// return normally without unwinding or throwing an exception, and must not 
call
++/// `lance_scanner_*` with the originating scanner. From callback entry until 
the
++/// enclosing operation that observes EOF has returned to its caller, the 
callback
++/// must not directly or indirectly cause any Arrow C stream derived from that
++/// scanner to be called, released, moved, destroyed, or otherwise accessed. 
This
++/// includes signaling or scheduling another thread to act based only on 
callback
++/// completion: the callback returns before the enclosing stream operation 
does.
++/// Such interaction is reentrant and has undefined behavior. Normal access 
may
++/// resume only after the enclosing `get_next`, `lance_scanner_next`, or
++/// `lance_scanner_poll_next` returns. Replacing the registration before 
scanning
++/// starts immediately releases the previous registration.
+ #[unsafe(no_mangle)]
+ pub unsafe extern "C" fn lance_scanner_set_statistics_callback(
+     scanner: *mut LanceScanner,
+@@ -732,7 +755,7 @@ pub unsafe extern "C" fn lance_scanner_close(scanner: *mut 
LanceScanner) {
+ /// Materialize the scan as an Arrow C Data Interface `ArrowArrayStream`.
+ ///
+ /// This is the preferred API for simple integrations — blocks the calling 
thread.
+-/// The scanner is consumed by this call and should not be used afterward 
(close it).
++/// The scanner remains valid and may be used to create additional streams.
+ ///
+ /// The exported stream is panic-guarded (issue #61): a panic during export
+ /// poisons the scanner — this call returns -1 with `LANCE_ERR_PANIC`, and
+diff --git a/tests/c_api_test.rs b/tests/c_api_test.rs
+index c1bb71d..daa7425 100644
+--- a/tests/c_api_test.rs
++++ b/tests/c_api_test.rs
+@@ -10,6 +10,7 @@ use std::ffi::{CString, c_char, c_void};
+ use std::process::Command;
+ use std::ptr;
+ use std::sync::Arc;
++use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering};
+ 
+ use arrow::ffi::from_ffi;
+ use arrow::ffi::{FFI_ArrowArray, FFI_ArrowSchema};
+@@ -151,6 +152,29 @@ unsafe extern "C" fn capture_scan_statistics(
+         .collect();
+ }
+ 
++#[derive(Default)]
++struct AtomicScanStatisticsCapture {
++    calls: AtomicUsize,
++    invalid_statistics: AtomicBool,
++}
++
++unsafe extern "C" fn capture_scan_statistics_atomically(
++    callback_ctx: *mut c_void,
++    statistics: *const LanceScanStatistics,
++) {
++    if callback_ctx.is_null() {
++        return;
++    }
++    let captured = unsafe { 
&*callback_ctx.cast::<AtomicScanStatisticsCapture>() };
++    if statistics.is_null() {
++        captured
++            .invalid_statistics
++            .store(true, AtomicOrdering::SeqCst);
++        return;
++    }
++    captured.calls.fetch_add(1, AtomicOrdering::SeqCst);
++}
++
+ /// Helper: build a tiny dataset whose `value` column is nullable AND contains
+ /// at least one NULL. Used by tests that need to exercise upstream's
+ /// nullability-tightening pre-scan failure path.
+@@ -413,7 +437,7 @@ fn 
test_scanner_statistics_callback_not_called_on_early_scanner_close() {
+ }
+ 
+ #[test]
+-fn test_scanner_statistics_callback_with_arrow_stream() {
++fn test_scanner_statistics_callback_applies_to_reused_scanner() {
+     let (_tmp, uri) = create_test_dataset();
+     let c_uri = c_str(&uri);
+     let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
+@@ -433,20 +457,87 @@ fn test_scanner_statistics_callback_with_arrow_stream() {
+         0
+     );
+ 
+-    let mut stream = FFI_ArrowArrayStream::empty();
++    let mut first_stream = FFI_ArrowArrayStream::empty();
+     assert_eq!(
+-        unsafe { lance_scanner_to_arrow_stream(scanner, &mut stream) },
++        unsafe { lance_scanner_to_arrow_stream(scanner, &mut first_stream) },
+         0
+     );
+-    let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut stream) 
}.unwrap();
++    let first_reader = unsafe { ArrowArrayStreamReader::from_raw(&mut 
first_stream) }.unwrap();
+     assert_eq!(
+-        reader.map(|batch| batch.unwrap().num_rows()).sum::<usize>(),
++        first_reader
++            .map(|batch| batch.unwrap().num_rows())
++            .sum::<usize>(),
+         5
+     );
+     assert_eq!(captured.calls, 1);
+-    assert!(captured.bytes_read > 0);
+ 
++    let mut second_stream = FFI_ArrowArrayStream::empty();
++    assert_eq!(
++        unsafe { lance_scanner_to_arrow_stream(scanner, &mut second_stream) },
++        0
++    );
+     unsafe { lance_scanner_close(scanner) };
++
++    let second_reader = unsafe { ArrowArrayStreamReader::from_raw(&mut 
second_stream) }.unwrap();
++    assert_eq!(
++        second_reader
++            .map(|batch| batch.unwrap().num_rows())
++            .sum::<usize>(),
++        5
++    );
++    assert_eq!(captured.calls, 2);
++
++    unsafe { lance_dataset_close(ds) };
++}
++
++#[test]
++fn test_scanner_statistics_callback_supports_concurrent_exported_streams() {
++    struct SendableArrowStream(FFI_ArrowArrayStream);
++    unsafe impl Send for SendableArrowStream {}
++
++    fn consume_stream(mut stream: SendableArrowStream) -> usize {
++        let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut stream.0) 
}.unwrap();
++        reader.map(|batch| batch.unwrap().num_rows()).sum()
++    }
++
++    let (_tmp, uri) = create_test_dataset();
++    let c_uri = c_str(&uri);
++    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
++    assert!(!ds.is_null());
++
++    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), ptr::null()) };
++    assert!(!scanner.is_null());
++    let captured = Arc::new(AtomicScanStatisticsCapture::default());
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics_atomically),
++                Arc::as_ptr(&captured).cast_mut().cast(),
++            )
++        },
++        0
++    );
++
++    let mut first_stream = FFI_ArrowArrayStream::empty();
++    let mut second_stream = FFI_ArrowArrayStream::empty();
++    assert_eq!(
++        unsafe { lance_scanner_to_arrow_stream(scanner, &mut first_stream) },
++        0
++    );
++    assert_eq!(
++        unsafe { lance_scanner_to_arrow_stream(scanner, &mut second_stream) },
++        0
++    );
++    unsafe { lance_scanner_close(scanner) };
++
++    let first = std::thread::spawn(move || 
consume_stream(SendableArrowStream(first_stream)));
++    let second = std::thread::spawn(move || 
consume_stream(SendableArrowStream(second_stream)));
++    assert_eq!(first.join().unwrap(), 5);
++    assert_eq!(second.join().unwrap(), 5);
++    assert_eq!(captured.calls.load(AtomicOrdering::SeqCst), 2);
++    assert!(!captured.invalid_statistics.load(AtomicOrdering::SeqCst));
++
+     unsafe { lance_dataset_close(ds) };
+ }
+ 
+@@ -546,6 +637,55 @@ fn test_scanner_statistics_callback_rejects_null_inputs() 
{
+     unsafe { lance_dataset_close(ds) };
+ }
+ 
++#[test]
++fn test_scanner_statistics_callback_replaces_registration_before_scan() {
++    let (_tmp, uri) = create_test_dataset();
++    let c_uri = c_str(&uri);
++    let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) };
++    assert!(!ds.is_null());
++    let scanner = unsafe { lance_scanner_new(ds, ptr::null(), ptr::null()) };
++    assert!(!scanner.is_null());
++
++    let mut replaced = CapturedScanStatistics::default();
++    let mut active = CapturedScanStatistics::default();
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics),
++                (&mut replaced as *mut CapturedScanStatistics).cast(),
++            )
++        },
++        0
++    );
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics),
++                (&mut active as *mut CapturedScanStatistics).cast(),
++            )
++        },
++        0
++    );
++
++    let mut stream = FFI_ArrowArrayStream::empty();
++    assert_eq!(
++        unsafe { lance_scanner_to_arrow_stream(scanner, &mut stream) },
++        0
++    );
++    let reader = unsafe { ArrowArrayStreamReader::from_raw(&mut stream) 
}.unwrap();
++    assert_eq!(
++        reader.map(|batch| batch.unwrap().num_rows()).sum::<usize>(),
++        5
++    );
++    assert_eq!(replaced.calls, 0);
++    assert_eq!(active.calls, 1);
++
++    unsafe { lance_scanner_close(scanner) };
++    unsafe { lance_dataset_close(ds) };
++}
++
+ #[test]
+ fn test_scanner_statistics_callback_rejects_registration_after_scan_started() 
{
+     let (_tmp, uri) = create_test_dataset();
+@@ -814,6 +954,17 @@ fn test_scanner_scan_async() {
+ 
+     let scanner = unsafe { lance_scanner_new(ds, ptr::null(), ptr::null()) };
+     assert!(!scanner.is_null());
++    let captured = Arc::new(AtomicScanStatisticsCapture::default());
++    assert_eq!(
++        unsafe {
++            lance_scanner_set_statistics_callback(
++                scanner,
++                Some(capture_scan_statistics_atomically),
++                Arc::as_ptr(&captured).cast_mut().cast(),
++            )
++        },
++        0
++    );
+ 
+     // Synchronization primitive for the async callback.
+     struct CallbackResult {
+@@ -845,6 +996,7 @@ fn test_scanner_scan_async() {
+             on_complete,
+             Arc::as_ptr(&pair_clone) as *mut std::ffi::c_void,
+         );
++        lance_scanner_close(scanner);
+     }
+ 
+     // Wait for callback.
+@@ -861,8 +1013,9 @@ fn test_scanner_scan_async() {
+     let reader = unsafe { ArrowArrayStreamReader::from_raw(ffi_stream) 
}.unwrap();
+     let total_rows: usize = reader.map(|r| r.unwrap().num_rows()).sum();
+     assert_eq!(total_rows, 5);
++    assert_eq!(captured.calls.load(AtomicOrdering::SeqCst), 1);
++    assert!(!captured.invalid_statistics.load(AtomicOrdering::SeqCst));
+ 
+-    unsafe { lance_scanner_close(scanner) };
+     unsafe { lance_dataset_close(ds) };
+ }
+ 
+@@ -1477,18 +1630,6 @@ fn test_poll_next_basic() {
+         let c_uri = c_str(&uri_clone);
+         let ds = unsafe { lance_dataset_open(c_uri.as_ptr(), ptr::null(), 0) 
};
+         let scanner = unsafe { lance_scanner_new(ds, ptr::null(), 
ptr::null()) };
+-        let mut captured = CapturedScanStatistics::default();
+-        assert_eq!(
+-            unsafe {
+-                lance_scanner_set_statistics_callback(
+-                    scanner,
+-                    Some(capture_scan_statistics),
+-                    (&mut captured as *mut CapturedScanStatistics).cast(),
+-                )
+-            },
+-            0
+-        );
+-
+         use std::sync::atomic::{AtomicBool, Ordering};
+         static WOKE: AtomicBool = AtomicBool::new(false);
+         unsafe extern "C" fn test_waker(_ctx: *mut std::ffi::c_void) {
+@@ -1522,15 +1663,6 @@ fn test_poll_next_basic() {
+             assert!(iterations < 1000, "poll loop should not spin forever");
+         }
+         assert_eq!(total_rows, 5);
+-        assert_eq!(captured.calls, 1);
+-
+-        let mut batch: *mut LanceBatch = ptr::null_mut();
+-        assert_eq!(
+-            unsafe { lance_scanner_poll_next(scanner, test_waker, 
ptr::null_mut(), &mut batch) },
+-            LancePollStatus::Finished
+-        );
+-        assert!(batch.is_null());
+-        assert_eq!(captured.calls, 1, "callback must run exactly once");
+ 
+         unsafe { lance_scanner_close(scanner) };
+         unsafe { lance_dataset_close(ds) };
+diff --git a/tests/cpp/test_c_api.c b/tests/cpp/test_c_api.c
+index b3b78f0..4499cb9 100644
+--- a/tests/cpp/test_c_api.c
++++ b/tests/cpp/test_c_api.c
+@@ -39,6 +39,36 @@
+         }                                                                     
 \
+     } while (0)
+ 
++typedef struct {
++    uint64_t calls;
++    uint64_t bytes_read;
++    int invalid;
++} ScanStatisticsCapture;
++
++static void capture_scan_statistics(
++    void *callback_ctx,
++    const LanceScanStatistics *statistics
++) {
++    if (callback_ctx == NULL) return;
++    ScanStatisticsCapture *captured = (ScanStatisticsCapture *)callback_ctx;
++    if (statistics == NULL ||
++        (statistics->metrics_len > 0 && statistics->metrics == NULL)) {
++        captured->invalid = 1;
++        return;
++    }
++    for (size_t i = 0; i < statistics->metrics_len; ++i) {
++        const LanceScanMetric *metric = &statistics->metrics[i];
++        if ((metric->name_len > 0 && metric->name == NULL) ||
++            (metric->kind != LANCE_SCAN_METRIC_COUNT &&
++             metric->kind != LANCE_SCAN_METRIC_TIME_NANOSECONDS)) {
++            captured->invalid = 1;
++            return;
++        }
++    }
++    captured->calls += 1;
++    captured->bytes_read = statistics->bytes_read;
++}
++
+ static void test_open_and_metadata(const char *uri) {
+     printf("  test_open_and_metadata... ");
+ 
+@@ -84,10 +114,14 @@ static void test_scan(const char *uri) {
+     /* Full scan via ArrowArrayStream */
+     LanceScanner *scanner = lance_scanner_new(ds, NULL, NULL);
+     ASSERT(scanner != NULL, "scanner creation failed");
++    ScanStatisticsCapture captured = {0};
++    int32_t rc = lance_scanner_set_statistics_callback(
++        scanner, capture_scan_statistics, &captured);
++    ASSERT(rc == 0, "statistics callback registration failed");
+ 
+     struct ArrowArrayStream stream;
+     memset(&stream, 0, sizeof(stream));
+-    int32_t rc = lance_scanner_to_arrow_stream(scanner, &stream);
++    rc = lance_scanner_to_arrow_stream(scanner, &stream);
+     ASSERT(rc == 0, "to_arrow_stream failed");
+ 
+     /* Read schema from stream */
+@@ -113,6 +147,9 @@ static void test_scan(const char *uri) {
+     }
+ 
+     ASSERT(total_rows == expected_rows, "row count mismatch");
++    ASSERT(captured.calls == 1, "statistics callback count mismatch");
++    ASSERT(captured.bytes_read > 0, "statistics should report bytes read");
++    ASSERT(captured.invalid == 0, "statistics callback received invalid 
data");
+     printf("rows=%llu... ", (unsigned long long)total_rows);
+ 
+     if (stream.release) stream.release(&stream);
+diff --git a/tests/cpp/test_cpp_api.cpp b/tests/cpp/test_cpp_api.cpp
+index f8ae701..3293bfb 100644
+--- a/tests/cpp/test_cpp_api.cpp
++++ b/tests/cpp/test_cpp_api.cpp
+@@ -25,6 +25,25 @@
+ #define TEST(name) printf("  %s... ", #name)
+ #define PASS()     printf("OK\n")
+ 
++struct ScanStatisticsCapture {
++    uint64_t calls = 0;
++    uint64_t bytes_read = 0;
++    bool invalid = false;
++};
++
++static void capture_scan_statistics(
++    void* callback_ctx,
++    const LanceScanStatistics* statistics) noexcept {
++    if (!callback_ctx) return;
++    auto* captured = static_cast<ScanStatisticsCapture*>(callback_ctx);
++    if (!statistics || (statistics->metrics_len > 0 && !statistics->metrics)) 
{
++        captured->invalid = true;
++        return;
++    }
++    captured->calls += 1;
++    captured->bytes_read = statistics->bytes_read;
++}
++
+ static void test_dataset_open(const std::string& uri) {
+     TEST(test_dataset_open);
+ 
+@@ -70,7 +89,11 @@ static void test_scanner_fluent(const std::string& uri) {
+ 
+     // Fluent builder pattern.
+     auto scanner = ds.scan();
+-    scanner.limit(5).offset(0).batch_size(2);
++    ScanStatisticsCapture captured;
++    scanner.limit(5)
++           .offset(0)
++           .batch_size(2)
++           .statistics_callback(capture_scan_statistics, &captured);
+ 
+     ArrowArrayStream stream;
+     memset(&stream, 0, sizeof(stream));
+@@ -89,6 +112,9 @@ static void test_scanner_fluent(const std::string& uri) {
+     }
+ 
+     assert(total == 5);
++    assert(captured.calls == 1);
++    assert(captured.bytes_read > 0);
++    assert(!captured.invalid);
+     printf("rows=%llu... ", (unsigned long long)total);
+ 
+     if (stream.release) stream.release(&stream);
diff --git a/thirdparty/vars.sh b/thirdparty/vars.sh
index 50834061fb5..27fca98f3f9 100644
--- a/thirdparty/vars.sh
+++ b/thirdparty/vars.sh
@@ -583,10 +583,10 @@ PUGIXML_SOURCE=pugixml-1.15
 PUGIXML_MD5SUM="3b894c29455eb33a40b165c6e2de5895"
 
 # lance-c
-LANCE_C_DOWNLOAD="https://github.com/lance-format/lance-c/archive/refs/tags/v0.1.6.tar.gz";
-LANCE_C_NAME="lance-c-v0.1.6.tar.gz"
-LANCE_C_SOURCE="lance-c-0.1.6"
-LANCE_C_MD5SUM="1599faa2532d9ce963db1188f7435a56"
+LANCE_C_DOWNLOAD="https://github.com/lance-format/lance-c/archive/refs/tags/v0.1.7.tar.gz";
+LANCE_C_NAME="lance-c-v0.1.7.tar.gz"
+LANCE_C_SOURCE="lance-c-0.1.7"
+LANCE_C_MD5SUM="15ef7cd20a2e1606384251cb2d41d42f"
 
 # all thirdparties which need to be downloaded is set in array TP_ARCHIVES
 export TP_ARCHIVES=(


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to