odbc_api/cursor.rs
1mod block_cursor;
2mod concurrent_block_cursor;
3mod polling_cursor;
4
5use log::warn;
6use odbc_sys::HStmt;
7
8use crate::{
9 Error, ResultSetMetadata,
10 buffers::Indicator,
11 error::ExtendResult,
12 handles::{
13 AsStatementRef, CDataMut, DiagnosticStream, SqlResult, State, Statement, StatementRef,
14 log_diagnostic_record,
15 },
16 parameter::{Binary, CElement, Text, VarCell, VarKind, WideText},
17};
18
19use std::{
20 mem::{MaybeUninit, size_of},
21 ptr,
22 thread::panicking,
23};
24
25pub use self::{
26 block_cursor::{BlockCursor, BlockCursorIterator},
27 concurrent_block_cursor::ConcurrentBlockCursor,
28 polling_cursor::{BlockCursorPolling, CursorPolling},
29};
30
31/// Cursors are used to process and iterate the result sets returned by executing queries.
32///
33/// # Example: Fetching result in batches
34///
35/// ```rust
36/// use odbc_api::{Cursor, buffers::{BufferDesc, ColumnarAnyBuffer}, Error};
37///
38/// /// Fetches all values from the first column of the cursor as i32 in batches of 100 and stores
39/// /// them in a vector.
40/// fn fetch_all_ints(cursor: impl Cursor) -> Result<Vec<i32>, Error> {
41/// let mut all_ints = Vec::new();
42/// // Batch size determines how many values we fetch at once.
43/// let batch_size = 100;
44/// // We expect the first column to hold INTEGERs (or a type convertible to INTEGER). Use
45/// // the metadata on the result set, if you want to investige the types of the columns at
46/// // runtime.
47/// let description = BufferDesc::I32 { nullable: false };
48/// // This is the buffer we bind to the driver, and repeatedly use to fetch each batch
49/// let buffer = ColumnarAnyBuffer::from_descs(batch_size, [description]);
50/// // Bind buffer to cursor
51/// let mut row_set_buffer = cursor.bind_buffer(buffer)?;
52/// // Fetch data batch by batch
53/// while let Some(batch) = row_set_buffer.fetch()? {
54/// all_ints.extend_from_slice(batch.column(0).as_slice().unwrap())
55/// }
56/// Ok(all_ints)
57/// }
58/// ```
59pub trait Cursor: ResultSetMetadata {
60 /// Advances the cursor to the next row in the result set. This is **Slow**. Bind
61 /// [`crate::buffers`] instead, for good performance.
62 ///
63 /// ⚠ While this method is very convenient due to the fact that the application does not have
64 /// to declare and bind specific buffers, it is also in many situations extremely slow. Concrete
65 /// performance depends on the ODBC driver in question, but it is likely it performs a roundtrip
66 /// to the datasource for each individual row. It is also likely an extra conversion is
67 /// performed then requesting individual fields, since the C buffer type is not known to the
68 /// driver in advance. Consider binding a buffer to the cursor first using
69 /// [`Self::bind_buffer`].
70 ///
71 /// That being said, it is a convenient programming model, as the developer does not need to
72 /// prepare and allocate the buffers beforehand. It is also a good way to retrieve really large
73 /// single values out of a data source (like one large text file). See [`CursorRow::get_text`].
74 fn next_row(&mut self) -> Result<Option<CursorRow<'_>>, Error> {
75 let row_available = unsafe {
76 self.as_stmt_ref()
77 .fetch()
78 .into_result_bool(&self.as_stmt_ref())?
79 };
80 let ret = if row_available {
81 Some(unsafe { CursorRow::new(self.as_stmt_ref()) })
82 } else {
83 None
84 };
85 Ok(ret)
86 }
87
88 /// Binds this cursor to a buffer holding a row set.
89 fn bind_buffer<B>(self, row_set_buffer: B) -> Result<BlockCursor<Self, B>, Error>
90 where
91 Self: Sized,
92 B: RowSetBuffer;
93
94 /// For some datasources it is possible to create more than one result set at once via a call to
95 /// execute. E.g. by calling a stored procedure or executing multiple SQL statements at once.
96 /// This method consumes the current cursor and creates a new one representing the next result
97 /// set should it exist.
98 fn more_results(self) -> Result<Option<Self>, Error>
99 where
100 Self: Sized;
101
102 /// Close the current cursor explicitly. Allows application to handle errors emitted by
103 /// `SQLCloseCursor`.
104 fn close(self) -> Result<(), Error>;
105}
106
107/// An individual row of an result set. See [`crate::Cursor::next_row`].
108pub struct CursorRow<'s> {
109 statement: StatementRef<'s>,
110}
111
112impl<'s> CursorRow<'s> {
113 /// # Safety
114 ///
115 /// `statement` must be in a cursor state.
116 unsafe fn new(statement: StatementRef<'s>) -> Self {
117 CursorRow { statement }
118 }
119}
120
121impl CursorRow<'_> {
122 /// Fills a suitable target buffer with a field from the current row of the result set. This
123 /// method drains the data from the field. It can be called repeatedly to if not all the data
124 /// fit in the output buffer at once. It should not called repeatedly to fetch the same value
125 /// twice. Column index starts at `1`.
126 ///
127 /// You can use [`crate::Nullable`] to fetch nullable values.
128 ///
129 /// # Example
130 ///
131 /// ```
132 /// # use odbc_api::{Cursor, Error, Nullable};
133 /// # fn fetch_values_example(cursor: &mut impl Cursor) -> Result<(), Error> {
134 /// // Declare nullable value to fetch value into. ODBC values layout is different from Rusts
135 /// // option. We can not use `Option<i32>` directly.
136 /// let mut field = Nullable::<i32>::null();
137 /// // Move cursor to next row
138 /// let mut row = cursor.next_row()?.unwrap();
139 /// // Fetch first column into field
140 /// row.get_data(1, &mut field)?;
141 /// // Convert nullable value to Option for convinience
142 /// let field = field.into_opt();
143 /// if let Some(value) = field {
144 /// println!("Value: {}", value);
145 /// } else {
146 /// println!("Value is NULL");
147 /// }
148 /// # Ok(())
149 /// # }
150 /// ```
151 pub fn get_data(
152 &mut self,
153 col_or_param_num: u16,
154 target: &mut (impl CElement + CDataMut),
155 ) -> Result<(), Error> {
156 self.statement
157 .get_data(col_or_param_num, target)
158 .into_result(&self.statement)
159 .provide_context_for_diagnostic(|record, function| {
160 if record.state == State::INDICATOR_VARIABLE_REQUIRED_BUT_NOT_SUPPLIED {
161 Error::UnableToRepresentNull(record)
162 } else {
163 Error::Diagnostics { record, function }
164 }
165 })
166 }
167
168 /// Retrieves arbitrary large character data from the row and stores it in the buffer. Column
169 /// index starts at `1`. The used encoding is accordig to the ODBC standard determined by your
170 /// system local. Ultimatly the choice is up to the implementation of your ODBC driver, which
171 /// often defaults to always UTF-8.
172 ///
173 /// # Example
174 ///
175 /// Retrieve an arbitrary large text file from a database field.
176 ///
177 /// ```
178 /// use odbc_api::{Connection, Error, IntoParameter, Cursor};
179 ///
180 /// fn get_large_text(name: &str, conn: &mut Connection<'_>) -> Result<Option<String>, Error> {
181 /// let query = "SELECT content FROM LargeFiles WHERE name=?";
182 /// let parameters = &name.into_parameter();
183 /// let timeout_sec = None;
184 /// let mut cursor = conn
185 /// .execute(query, parameters, timeout_sec)?
186 /// .expect("Assume select statement creates cursor");
187 /// if let Some(mut row) = cursor.next_row()? {
188 /// let mut buf = Vec::new();
189 /// row.get_text(1, &mut buf)?;
190 /// let ret = String::from_utf8(buf).unwrap();
191 /// Ok(Some(ret))
192 /// } else {
193 /// Ok(None)
194 /// }
195 /// }
196 /// ```
197 ///
198 /// # Return
199 ///
200 /// `true` indicates that the value has not been `NULL` and the value has been placed in `buf`.
201 /// `false` indicates that the value is `NULL`. The buffer is cleared in that case.
202 pub fn get_text(&mut self, col_or_param_num: u16, buf: &mut Vec<u8>) -> Result<bool, Error> {
203 self.get_variadic::<Text>(col_or_param_num, buf)
204 }
205
206 /// Retrieves arbitrary large character data from the row and stores it in the buffer. Column
207 /// index starts at `1`. The used encoding is UTF-16.
208 ///
209 /// # Return
210 ///
211 /// `true` indicates that the value has not been `NULL` and the value has been placed in `buf`.
212 /// `false` indicates that the value is `NULL`. The buffer is cleared in that case.
213 pub fn get_wide_text(
214 &mut self,
215 col_or_param_num: u16,
216 buf: &mut Vec<u16>,
217 ) -> Result<bool, Error> {
218 self.get_variadic::<WideText>(col_or_param_num, buf)
219 }
220
221 /// Retrieves arbitrary large binary data from the row and stores it in the buffer. Column index
222 /// starts at `1`.
223 ///
224 /// # Return
225 ///
226 /// `true` indicates that the value has not been `NULL` and the value has been placed in `buf`.
227 /// `false` indicates that the value is `NULL`. The buffer is cleared in that case.
228 pub fn get_binary(&mut self, col_or_param_num: u16, buf: &mut Vec<u8>) -> Result<bool, Error> {
229 self.get_variadic::<Binary>(col_or_param_num, buf)
230 }
231
232 fn get_variadic<K: VarKind>(
233 &mut self,
234 col_or_param_num: u16,
235 buf: &mut Vec<K::Element>,
236 ) -> Result<bool, Error> {
237 if buf.capacity() == 0 {
238 // User did just provide an empty buffer. So it is fair to assume not much domain
239 // knowledge has been used to decide its size. We just default to 256 to increase the
240 // chance that we get it done with one alloctaion. The buffer size being 0 we need at
241 // least 1 anyway. If the capacity is not `0` we'll leave the buffer size untouched as
242 // we do not want to prevent users from providing better guessen based on domain
243 // knowledge.
244 // This also implicitly makes sure that we can at least hold one terminating zero.
245 buf.reserve(256);
246 }
247 // Utilize all of the allocated buffer.
248 buf.resize(buf.capacity(), K::ZERO);
249
250 // Did we learn how much capacity we need in the last iteration? We use this only to panic
251 // on erroneous implementations of get_data and avoid endless looping until we run out of
252 // memory.
253 let mut remaining_length_known = false;
254 // We repeatedly fetch data and add it to the buffer. The buffer length is therefore the
255 // accumulated value size. The target always points to the last window in buf which is going
256 // to contain the **next** part of the data, whereas buf contains the entire accumulated
257 // value so far.
258 let mut target =
259 VarCell::<&mut [K::Element], K>::from_buffer(buf.as_mut_slice(), Indicator::NoTotal);
260 self.get_data(col_or_param_num, &mut target)?;
261 while !target.is_complete() {
262 // Amount of payload bytes (excluding terminating zeros) fetched with the last call to
263 // get_data.
264 let fetched = target
265 .len_in_bytes()
266 .expect("ODBC driver must always report how many bytes were fetched.");
267 match target.indicator() {
268 // If Null the value would be complete
269 Indicator::Null => unreachable!(),
270 // We do not know how large the value is. Let's fetch the data with repeated calls
271 // to get_data.
272 Indicator::NoTotal => {
273 let old_len = buf.len();
274 // Use an exponential strategy for increasing buffer size.
275 buf.resize(old_len * 2, K::ZERO);
276 let buf_extend = &mut buf[(old_len - K::TERMINATING_ZEROES)..];
277 target = VarCell::<&mut [K::Element], K>::from_buffer(
278 buf_extend,
279 Indicator::NoTotal,
280 );
281 }
282 // We did not get all of the value in one go, but the data source has been friendly
283 // enough to tell us how much is missing.
284 Indicator::Length(len) => {
285 if remaining_length_known {
286 panic!(
287 "SQLGetData has been unable to fetch all data, even though the \
288 capacity of the target buffer has been adapted to hold the entire \
289 payload based on the indicator of the last part. You may consider \
290 filing a bug with the ODBC driver you are using."
291 )
292 }
293 remaining_length_known = true;
294 // Amount of bytes missing from the value using get_data, excluding terminating
295 // zero.
296 let still_missing_in_bytes = len - fetched;
297 let still_missing = still_missing_in_bytes / size_of::<K::Element>();
298 let old_len = buf.len();
299 buf.resize(old_len + still_missing, K::ZERO);
300 let buf_extend = &mut buf[(old_len - K::TERMINATING_ZEROES)..];
301 target = VarCell::<&mut [K::Element], K>::from_buffer(
302 buf_extend,
303 Indicator::NoTotal,
304 );
305 }
306 }
307 // Fetch binary data into buffer.
308 self.get_data(col_or_param_num, &mut target)?;
309 }
310 // We did get the complete value, including the terminating zero. Let's resize the buffer to
311 // match the retrieved value exactly (excluding terminating zero).
312 if let Some(len_in_bytes) = target.indicator().length() {
313 // Since the indicator refers to value length without terminating zero, and capacity is
314 // including the terminating zero this also implicitly drops the terminating zero at the
315 // end of the buffer.
316 let shrink_by_bytes = target.capacity_in_bytes() - len_in_bytes;
317 let shrink_by_chars = shrink_by_bytes / size_of::<K::Element>();
318 buf.resize(buf.len() - shrink_by_chars, K::ZERO);
319 Ok(true)
320 } else {
321 // value is NULL
322 buf.clear();
323 Ok(false)
324 }
325 }
326}
327
328/// Cursors are used to process and iterate the result sets returned by executing queries. Created
329/// by either a prepared query or direct execution. Usually utilized through the [`crate::Cursor`]
330/// trait.
331#[derive(Debug)]
332pub struct CursorImpl<Stmt: AsStatementRef> {
333 /// A statement handle in cursor mode.
334 statement: Stmt,
335}
336
337impl<S> Drop for CursorImpl<S>
338where
339 S: AsStatementRef,
340{
341 fn drop(&mut self) {
342 let mut stmt = self.statement.as_stmt_ref();
343 if let Err(e) = stmt.close_cursor().into_result(&stmt) {
344 // Avoid panicking, if we already have a panic. We don't want to mask the original
345 // error.
346 if !panicking() {
347 panic!("Unexpected error closing cursor: {e:?}")
348 }
349 }
350 }
351}
352
353impl<S> AsStatementRef for CursorImpl<S>
354where
355 S: AsStatementRef,
356{
357 fn as_stmt_ref(&mut self) -> StatementRef<'_> {
358 self.statement.as_stmt_ref()
359 }
360}
361
362impl<S> ResultSetMetadata for CursorImpl<S> where S: AsStatementRef {}
363
364impl<S> Cursor for CursorImpl<S>
365where
366 S: AsStatementRef,
367{
368 fn bind_buffer<B>(mut self, mut row_set_buffer: B) -> Result<BlockCursor<Self, B>, Error>
369 where
370 B: RowSetBuffer,
371 {
372 let stmt = self.statement.as_stmt_ref();
373 unsafe {
374 bind_row_set_buffer_to_statement(stmt, &mut row_set_buffer)?;
375 }
376 Ok(BlockCursor::new(row_set_buffer, self))
377 }
378
379 fn more_results(self) -> Result<Option<Self>, Error>
380 where
381 Self: Sized,
382 {
383 // Consume self without calling drop to avoid calling close_cursor.
384 let mut statement = self.into_stmt();
385 let mut stmt = statement.as_stmt_ref();
386
387 let has_another_result = unsafe { stmt.more_results() }.into_result_bool(&stmt)?;
388 let next = if has_another_result {
389 Some(CursorImpl { statement })
390 } else {
391 None
392 };
393 Ok(next)
394 }
395
396 fn close(self) -> Result<(), Error> {
397 let mut stmt = self.into_stmt();
398 let mut stmt = stmt.as_stmt_ref();
399 stmt.close_cursor().into_result(&stmt)?;
400 Ok(())
401 }
402}
403
404impl<S> CursorImpl<S>
405where
406 S: AsStatementRef,
407{
408 /// Users of this library are encouraged not to call this constructor directly but rather invoke
409 /// [`crate::Connection::execute`] or [`crate::Prepared::execute`] to get a cursor and utilize
410 /// it using the [`crate::Cursor`] trait. This method is public so users with an understanding
411 /// of the raw ODBC C-API have a way to create a cursor, after they left the safety rails of the
412 /// Rust type System, in order to implement a use case not covered yet, by the safe abstractions
413 /// within this crate.
414 ///
415 /// # Safety
416 ///
417 /// `statement` must be in Cursor state, for the invariants of this type to hold.
418 pub unsafe fn new(statement: S) -> Self {
419 Self { statement }
420 }
421
422 /// Deconstructs the `CursorImpl` without calling drop. This is a way to get to the underlying
423 /// statement, while preventing a call to close cursor.
424 pub fn into_stmt(self) -> S {
425 // We want to move `statement` out of self, which would make self partially uninitialized.
426 let dont_drop_me = MaybeUninit::new(self);
427 let self_ptr = dont_drop_me.as_ptr();
428
429 // Safety: We know `dont_drop_me` is valid at this point so reading the ptr is okay
430 unsafe { ptr::read(&(*self_ptr).statement) }
431 }
432
433 pub(crate) fn as_sys(&mut self) -> HStmt {
434 self.as_stmt_ref().as_sys()
435 }
436}
437
438/// A Row set buffer binds row, or column wise buffers to a cursor in order to fill them with row
439/// sets with each call to fetch.
440///
441/// # Safety
442///
443/// Implementers of this trait must ensure that every pointer bound in `bind_to_cursor` stays valid
444/// even if an instance is moved in memory. Bound members should therefore be likely references
445/// themselves. To bind stack allocated buffers it is recommended to implement this trait on the
446/// reference type instead.
447pub unsafe trait RowSetBuffer {
448 /// Declares the bind type of the Row set buffer. `0` Means a columnar binding is used. Any non
449 /// zero number is interpreted as the size of a single row in a row wise binding style.
450 fn bind_type(&self) -> usize;
451
452 /// The batch size for bulk cursors, if retrieving many rows at once.
453 fn row_array_size(&self) -> usize;
454
455 /// Mutable reference to the number of fetched rows.
456 ///
457 /// # Safety
458 ///
459 /// Implementations of this method must take care that the returned referenced stays valid, even
460 /// if `self` should be moved.
461 fn mut_num_fetch_rows(&mut self) -> &mut usize;
462
463 /// Binds the buffer either column or row wise to the cursor.
464 ///
465 /// # Safety
466 ///
467 /// It's the implementation's responsibility to ensure that all bound buffers are valid until
468 /// unbound or the statement handle is deleted.
469 unsafe fn bind_colmuns_to_cursor(&mut self, cursor: StatementRef<'_>) -> Result<(), Error>;
470
471 /// Find an indicator larger than the maximum element size of the buffer.
472 fn find_truncation(&self) -> Option<TruncationInfo>;
473}
474
475/// Returned by [`RowSetBuffer::find_truncation`]. Contains information about the truncation found.
476#[derive(Clone, Copy, PartialEq, Eq, Debug)]
477pub struct TruncationInfo {
478 /// Length of the untruncated value if known
479 pub indicator: Option<usize>,
480 /// Zero based buffer index of the column in which the truncation occurred.
481 pub buffer_index: usize,
482}
483
484unsafe impl<T: RowSetBuffer> RowSetBuffer for &mut T {
485 fn bind_type(&self) -> usize {
486 (**self).bind_type()
487 }
488
489 fn row_array_size(&self) -> usize {
490 (**self).row_array_size()
491 }
492
493 fn mut_num_fetch_rows(&mut self) -> &mut usize {
494 (*self).mut_num_fetch_rows()
495 }
496
497 unsafe fn bind_colmuns_to_cursor(&mut self, cursor: StatementRef<'_>) -> Result<(), Error> {
498 unsafe { (*self).bind_colmuns_to_cursor(cursor) }
499 }
500
501 fn find_truncation(&self) -> Option<TruncationInfo> {
502 (**self).find_truncation()
503 }
504}
505
506/// Binds a row set buffer to a statment. Implementation is shared between synchronous and
507/// asynchronous cursors.
508unsafe fn bind_row_set_buffer_to_statement(
509 mut stmt: StatementRef<'_>,
510 row_set_buffer: &mut impl RowSetBuffer,
511) -> Result<(), Error> {
512 unsafe {
513 stmt.set_row_bind_type(row_set_buffer.bind_type())
514 .into_result(&stmt)?;
515 let size = row_set_buffer.row_array_size();
516 let sql_result = stmt.set_row_array_size(size);
517
518 // Search for "option value changed". A QODBC driver reported "option value changed", yet
519 // set the value to `723477590136`. We want to panic if something like this happens.
520 //
521 // See: <https://github.com/pacman82/odbc-api/discussions/742#discussioncomment-13887516>
522 let mut diagnostic_stream = DiagnosticStream::new(&stmt);
523 // We just rememeber that we have seen "option value changed", before asking for the array
524 // size, in order to not mess with other diagnostic records.
525 let mut option_value_changed = false;
526 while let Some(record) = diagnostic_stream.next() {
527 log_diagnostic_record(record);
528 if record.state == State::OPTION_VALUE_CHANGED {
529 option_value_changed = true;
530 }
531 }
532 if option_value_changed {
533 // Now rejecting a too large buffer size is save, but not if the value is something
534 // even larger after. Zero is also suspicious.
535 let actual_size = stmt.row_array_size().into_result(&stmt)?;
536 #[cfg(not(feature = "structured_logging"))]
537 warn!(
538 "Row array size set by the driver to: {actual_size}. Desired size had been: {size}"
539 );
540 #[cfg(feature = "structured_logging")]
541 warn!(
542 target: "odbc_api",
543 requested = size,
544 actual = actual_size;
545 "Row array size overridden by driver"
546 );
547 if actual_size > size || actual_size == 0 {
548 panic!(
549 "Your ODBC buffer changed the array size for bulk fetchin in an unsound way. \
550 To prevent undefined behavior the application must panic. You can try \
551 different batch sizes for bulk fetching, or report a bug with your ODBC driver \
552 provider. This behavior has been observed with QODBC drivers. If you are using \
553 one try fetching row by row rather than the faster bulk fetch."
554 )
555 }
556 }
557
558 sql_result
559 // We already logged diagnostic records then we were looking for Option value changed
560 .into_result_without_logging(&stmt)
561 // SAP anywhere has been seen to return with an "invalid attribute" error instead of
562 // a success with "option value changed" info. Let us map invalid attributes during
563 // setting row set array size to something more precise.
564 .provide_context_for_diagnostic(|record, function| {
565 if record.state == State::INVALID_ATTRIBUTE_VALUE {
566 Error::InvalidRowArraySize { record, size }
567 } else {
568 Error::Diagnostics { record, function }
569 }
570 })?;
571 stmt.set_num_rows_fetched(row_set_buffer.mut_num_fetch_rows())
572 .into_result(&stmt)?;
573 row_set_buffer.bind_colmuns_to_cursor(stmt)?;
574 Ok(())
575 }
576}
577
578/// Error handling for bulk fetching is shared between synchronous and asynchronous usecase.
579fn error_handling_for_fetch(
580 result: SqlResult<()>,
581 mut stmt: StatementRef,
582 buffer: &impl RowSetBuffer,
583 error_for_truncation: bool,
584) -> Result<bool, Error> {
585 // Only check for truncation if a) the user indicated that he wants to error instead of just
586 // ignoring it and if there is at least one diagnostic record. ODBC standard requires a
587 // diagnostic record to be there in case of truncation. Sadly we can not rely on this particular
588 // record to be there, as the driver could generate a large amount of diagnostic records,
589 // while we are limited in the amount we can check. The second check serves as an optimization
590 // for the happy path.
591 if error_for_truncation
592 && result == SqlResult::SuccessWithInfo(())
593 && let Some(TruncationInfo {
594 indicator,
595 buffer_index,
596 }) = buffer.find_truncation()
597 {
598 return Err(Error::TooLargeValueForBuffer {
599 indicator,
600 buffer_index,
601 });
602 }
603
604 let has_row = result
605 .on_success(|| true)
606 .on_no_data(|| false)
607 .into_result(&stmt.as_stmt_ref())
608 // Oracle's ODBC driver does not support 64Bit integers. Furthermore, it does not
609 // tell it to the user when binding parameters, but rather now then we fetch
610 // results. The error code returned is `HY004` rather than `HY003` which should
611 // be used to indicate invalid buffer types.
612 .provide_context_for_diagnostic(|record, function| {
613 if record.state == State::INVALID_SQL_DATA_TYPE {
614 Error::OracleOdbcDriverDoesNotSupport64Bit(record)
615 } else {
616 Error::Diagnostics { record, function }
617 }
618 })?;
619 Ok(has_row)
620}
621
622/// Unbinds buffer and num_rows_fetched from the cursor. This implementation is shared between
623/// unbind and the drop handler, and the synchronous and asynchronous variant.
624fn unbind_buffer_from_cursor(cursor: &mut impl AsStatementRef) -> Result<(), Error> {
625 // Now that we have cursor out of block cursor, we need to unbind the buffer.
626 let mut stmt = cursor.as_stmt_ref();
627 stmt.unbind_cols().into_result(&stmt)?;
628 stmt.unset_num_rows_fetched().into_result(&stmt)?;
629 Ok(())
630}