better_duck_core/raw/data_chunk.rs
1use std::{
2 ops::{Deref, DerefMut},
3 ptr,
4};
5
6use crate::ffi::{duckdb_data_chunk, duckdb_data_chunk_reset, duckdb_destroy_data_chunk};
7
8use super::result::DuckResult;
9use crate::{error::Result, ffi};
10
11/// An owned DuckDB `duckdb_data_chunk` — a batch of column vectors — that is
12/// destroyed on drop.
13///
14/// Obtain one from a query result with [`DataChunk::from_result`], then append it
15/// elsewhere with [`Appender::append_chunk`](crate::Appender::append_chunk).
16pub struct DataChunk(
17 pub(crate) duckdb_data_chunk,
18 pub(crate) u64, // current row index in chunk
19);
20
21impl DataChunk {
22 /// Takes ownership of a raw `duckdb_data_chunk` handle.
23 ///
24 /// # Errors
25 ///
26 /// Returns an error if `data_chunk` is null.
27 #[inline]
28 pub fn new(data_chunk: ffi::duckdb_data_chunk) -> Result<DataChunk> {
29 if data_chunk.is_null() {
30 return Err(crate::error::Error::DuckDBFailure(
31 ffi::Error::new(ffi::DuckDBError),
32 Some("data chunk is null".to_owned()),
33 ));
34 }
35 Ok(DataChunk(data_chunk, 0))
36 }
37 /// Fetches the next chunk of a query result, or `None` once the result is
38 /// exhausted.
39 #[inline]
40 pub fn from_result(result: &DuckResult) -> Option<Result<DataChunk>> {
41 // SAFETY: `result` is a valid duckdb_result; the returned chunk (if non-null)
42 // is owned by us and must be destroyed via `duckdb_destroy_data_chunk`.
43 let data_chunk = unsafe { ffi::duckdb_fetch_chunk(**result) };
44 if data_chunk.is_null() {
45 return None;
46 }
47 // SAFETY: `data_chunk` is non-null and freshly obtained from `duckdb_fetch_chunk`.
48 let res = DataChunk::new(data_chunk);
49 Some(res)
50 }
51
52 /// The current row cursor used by [`next_row`](DataChunk::next_row).
53 #[allow(unused)]
54 #[inline]
55 pub fn current_row(&self) -> u64 {
56 self.1
57 }
58 /// The number of rows in the chunk.
59 #[inline]
60 pub fn row_count(&self) -> u64 {
61 // SAFETY: `self.0` is a valid duckdb_data_chunk (enforced by the caller).
62 unsafe { ffi::duckdb_data_chunk_get_size(self.0) }
63 }
64
65 /// Resets the chunk to empty (row count 0), keeping its allocated capacity so it
66 /// can be refilled and re-appended.
67 #[inline]
68 pub fn reset(&mut self) {
69 // SAFETY: `self.0` is a valid, non-null duckdb_data_chunk owned by `self`.
70 unsafe { duckdb_data_chunk_reset(self.0) };
71 self.1 = 0;
72 }
73
74 /// Advances the row cursor, returning the next row index, or `None` at the end
75 /// (destroying the underlying chunk).
76 #[inline]
77 pub fn next_row(&mut self) -> Option<u64> {
78 // SAFETY: `self.0` is a valid duckdb_data_chunk (enforced by the caller).
79 if self.row_count() < 1 {
80 return None;
81 }
82 // SAFETY: same as above.
83 if self.1 >= self.row_count() {
84 // Reset the row index and fetch the next chunk
85 self.1 = 0;
86 // SAFETY: `self.0` is a valid, non-null duckdb_data_chunk; after destroy
87 // we null it so this path is never re-entered.
88 unsafe { duckdb_destroy_data_chunk(&mut (self.0)) };
89 self.0 = ptr::null_mut();
90 return None;
91 }
92 self.1 += 1;
93 Some(self.1 - 1)
94 }
95}
96
97impl Deref for DataChunk {
98 type Target = duckdb_data_chunk;
99
100 #[inline]
101 fn deref(&self) -> &Self::Target {
102 &self.0
103 }
104}
105
106impl DerefMut for DataChunk {
107 #[inline]
108 fn deref_mut(&mut self) -> &mut Self::Target {
109 &mut self.0
110 }
111}
112
113impl Drop for DataChunk {
114 fn drop(&mut self) {
115 if !self.0.is_null() {
116 // SAFETY: `self.0` is a valid non-null `duckdb_data_chunk`. The null guard
117 // ensures this path runs at most once.
118 unsafe { duckdb_destroy_data_chunk(&mut (self.0)) };
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::types::LogicalType;
127
128 #[test]
129 fn reset_clears_the_row_count() {
130 // Build a one-column INTEGER chunk and give it three rows.
131 let int_ty = LogicalType::of::<i32>().unwrap();
132 let mut raw_types = [int_ty.as_raw()];
133 // SAFETY: `raw_types` holds one valid logical type handle for the call; DuckDB
134 // copies it. The returned chunk is wrapped in RAII (destroyed once on drop).
135 let raw = unsafe { ffi::duckdb_create_data_chunk(raw_types.as_mut_ptr(), 1) };
136 drop(int_ty);
137 let mut chunk = DataChunk::new(raw).unwrap();
138 // SAFETY: `chunk` is valid; sizing to 3 rows is within the default capacity.
139 unsafe { ffi::duckdb_data_chunk_set_size(*chunk, 3) };
140 assert_eq!(chunk.row_count(), 3);
141
142 chunk.reset();
143 assert_eq!(chunk.row_count(), 0, "reset must clear the chunk to empty");
144 }
145}