odbc_api/parameter/
blob.rs

1use odbc_sys::{CDataType, DATA_AT_EXEC, len_data_at_exec};
2
3use crate::{
4    DataType, Error, ParameterCollection, ParameterTupleElement,
5    handles::{DelayedInput, HasDataType, Statement},
6};
7use std::{
8    ffi::c_void,
9    fs::File,
10    io::{self, BufRead, BufReader},
11    num::NonZeroUsize,
12    path::Path,
13};
14
15/// A `Blob` can stream its contents to the database batch by batch and may therefore be used to
16/// transfer large amounts of data, exceeding the drivers capabilities for normal input parameters.
17///
18/// # Safety
19///
20/// If a hint is implemented for `blob_size` it must be accurate before the first call to
21/// `next_batch`.
22pub unsafe trait Blob: HasDataType {
23    /// CData type of the binary data returned in the batches. Likely to be either
24    /// [`crate::sys::CDataType::Binary`], [`crate::sys::CDataType::Char`] or
25    /// [`crate::sys::CDataType::WChar`].
26    fn c_data_type(&self) -> CDataType;
27
28    /// Hint passed on to the driver regarding the combined size of all the batches. This hint is
29    /// passed then the parameter is bound to the statement, so its meaning is only defined before
30    /// the first call to `next_batch`. If `None` no hint about the total length of the batches is
31    /// passed to the driver and the indicator will be set to [`crate::sys::DATA_AT_EXEC`].
32    fn size_hint(&self) -> Option<usize>;
33
34    /// Retrieve the next batch of data from the source. Batches may not be empty. `None` indicates
35    /// the last batch has been reached.
36    fn next_batch(&mut self) -> io::Result<Option<&[u8]>>;
37
38    /// Convinience function. Same as calling [`self::BlobParam::new`].
39    fn as_blob_param(&mut self) -> BlobParam
40    where
41        Self: Sized,
42    {
43        BlobParam::new(self)
44    }
45}
46
47/// Parameter type which can be used to bind a [`self::Blob`] as parameter to a statement in order
48/// for its contents to be streamed to the database at statement execution time.
49pub struct BlobParam<'a> {
50    /// Should be [`crate::sys::DATA_AT_EXEC`] if no size hint is given, or the result of
51    /// [`crate::sys::len_data_at_exec`].
52    indicator: isize,
53    /// Trait object to be bound as a delayed parameter.
54    blob: &'a mut dyn Blob,
55}
56
57impl<'a> BlobParam<'a> {
58    pub fn new(blob: &'a mut impl Blob) -> Self {
59        let indicator = if let Some(size) = blob.size_hint() {
60            len_data_at_exec(size.try_into().unwrap())
61        } else {
62            DATA_AT_EXEC
63        };
64        Self { indicator, blob }
65    }
66}
67
68unsafe impl DelayedInput for BlobParam<'_> {
69    fn cdata_type(&self) -> CDataType {
70        self.blob.c_data_type()
71    }
72
73    fn indicator_ptr(&self) -> *const isize {
74        &self.indicator as *const isize
75    }
76
77    fn stream_ptr(&mut self) -> *mut c_void {
78        // Types must have the same size for the transmute to work in the reverse cast.
79        debug_assert_eq!(
80            std::mem::size_of::<*mut &mut dyn Blob>(),
81            std::mem::size_of::<*mut c_void>()
82        );
83        &mut self.blob as *mut &mut dyn Blob as *mut c_void
84    }
85}
86
87impl HasDataType for BlobParam<'_> {
88    fn data_type(&self) -> DataType {
89        self.blob.data_type()
90    }
91}
92
93unsafe impl ParameterCollection for BlobParam<'_> {
94    fn parameter_set_size(&self) -> usize {
95        1
96    }
97
98    unsafe fn bind_parameters_to(&mut self, stmt: &mut impl Statement) -> Result<(), Error> {
99        unsafe { stmt.bind_delayed_input_parameter(1, self) }.into_result(stmt)
100    }
101}
102
103unsafe impl ParameterTupleElement for &mut BlobParam<'_> {
104    unsafe fn bind_to(
105        &mut self,
106        parameter_number: u16,
107        stmt: &mut impl Statement,
108    ) -> Result<(), Error> {
109        unsafe { stmt.bind_delayed_input_parameter(parameter_number, *self) }.into_result(stmt)
110    }
111}
112
113/// Wraps borrowed bytes with a batch_size and implements [`self::Blob`]. Use this type to send long
114/// array of bytes to the database.
115pub struct BlobSlice<'a> {
116    /// If `true` the blob is going to be bound as [`DataType::LongVarbinary`] and the bytes are
117    /// interpreted as [`CDataType::Binary`]. If false the blob is going to be bound as
118    /// [`DataType::LongVarchar`] and the bytes are interpreted as [`CDataType::Char`].
119    pub is_binary: bool,
120    /// Maximum number of bytes transferred to the database in one go. May be largere than the
121    /// remaining blob size.
122    pub batch_size: usize,
123    /// Remaining bytes to transfer to the database.
124    pub blob: &'a [u8],
125}
126
127impl<'a> BlobSlice<'a> {
128    /// Construct a Blob from a byte slice. The blob is going to be bound as a `LongVarbinary` and
129    /// will be transmitted in one batch.
130    ///
131    /// # Example
132    ///
133    /// ```
134    /// use odbc_api::{Connection, parameter::{Blob, BlobSlice}, IntoParameter, Error};
135    ///
136    /// fn insert_image(
137    ///     conn: &Connection<'_>,
138    ///     id: &str,
139    ///     image_data: &[u8]
140    /// ) -> Result<(), Error>
141    /// {
142    ///     let mut blob = BlobSlice::from_byte_slice(image_data);
143    ///
144    ///     let insert = "INSERT INTO Images (id, image_data) VALUES (?,?)";
145    ///     let parameters = (&id.into_parameter(), &mut blob.as_blob_param());
146    ///     conn.execute(&insert, parameters, None)?;
147    ///     Ok(())
148    /// }
149    /// ```
150    pub fn from_byte_slice(blob: &'a [u8]) -> Self {
151        Self {
152            is_binary: true,
153            batch_size: blob.len(),
154            blob,
155        }
156    }
157
158    /// Construct a Blob from a text slice. The blob is going to be bound as a `LongVarchar` and
159    /// will be transmitted in one batch.
160    ///
161    /// # Example
162    ///
163    /// This example insert `title` as a normal input parameter but streams the potentially much
164    /// longer `String` in `text` to the database as a large text blob. This allows to circumvent
165    /// the size restrictions for `String` arguments of many drivers (usually around 4 or 8 KiB).
166    ///
167    /// ```
168    /// use odbc_api::{Connection, parameter::{Blob, BlobSlice}, IntoParameter, Error};
169    ///
170    /// fn insert_book(
171    ///     conn: &Connection<'_>,
172    ///     title: &str,
173    ///     text: &str
174    /// ) -> Result<(), Error>
175    /// {
176    ///     let mut blob = BlobSlice::from_text(&text);
177    ///
178    ///     let insert = "INSERT INTO Books (title, text) VALUES (?,?)";
179    ///     let parameters = (&title.into_parameter(), &mut blob.as_blob_param());
180    ///     conn.execute(&insert, parameters, None)?;
181    ///     Ok(())
182    /// }
183    /// ```
184    pub fn from_text(text: &'a str) -> Self {
185        Self {
186            is_binary: false,
187            batch_size: text.len(),
188            blob: text.as_bytes(),
189        }
190    }
191}
192
193impl HasDataType for BlobSlice<'_> {
194    fn data_type(&self) -> DataType {
195        if self.is_binary {
196            DataType::LongVarbinary {
197                length: NonZeroUsize::new(self.blob.len()),
198            }
199        } else {
200            DataType::LongVarchar {
201                length: NonZeroUsize::new(self.blob.len()),
202            }
203        }
204    }
205}
206
207unsafe impl Blob for BlobSlice<'_> {
208    fn c_data_type(&self) -> CDataType {
209        if self.is_binary {
210            CDataType::Binary
211        } else {
212            CDataType::Char
213        }
214    }
215
216    fn size_hint(&self) -> Option<usize> {
217        Some(self.blob.len())
218    }
219
220    fn next_batch(&mut self) -> io::Result<Option<&[u8]>> {
221        if self.blob.is_empty() {
222            return Ok(None);
223        }
224
225        if self.blob.len() >= self.batch_size {
226            let (head, tail) = self.blob.split_at(self.batch_size);
227            self.blob = tail;
228            Ok(Some(head))
229        } else {
230            let last_batch = self.blob;
231            self.blob = &[];
232            Ok(Some(last_batch))
233        }
234    }
235}
236
237/// Wraps an [`std::io::BufRead`] and implements [`self::Blob`]. Use this to stream contents from an
238/// [`std::io::BufRead`] to the database. The blob implementation is going to directly utilize the
239/// Buffer of the [`std::io::BufRead`] implementation, so the batch size is likely equal to that
240/// capacity.
241pub struct BlobRead<R> {
242    /// `true` if `size` is to interpreted as the exact ammount of bytes contained in the reader, at
243    /// the time of binding it as a parameter. `false` if `size` is to be interpreted as an upper
244    /// bound.
245    exact: bool,
246    size: usize,
247    consume: usize,
248    buf_read: R,
249}
250
251impl<R> BlobRead<R> {
252    /// Construct a blob read from any [`std::io::BufRead`]. The `upper bound` is used in the type
253    /// description then binding the blob as a parameter.
254    ///
255    /// # Examples
256    ///
257    /// This is more flexible than [`Self::from_path`]. Note however that files provide metadata
258    /// about the length of the data, which `io::BufRead` does not. This is not an issue for most
259    /// drivers, but some can perform optimization if they know the size in advance. In the tests
260    /// SQLite has shown a bug to only insert empty data if no size hint has been provided.
261    ///
262    /// ```
263    /// use std::io::BufRead;
264    /// use odbc_api::{Connection, parameter::{Blob, BlobRead}, IntoParameter, Error};
265    ///
266    /// fn insert_image_to_db(
267    ///     conn: &Connection<'_>,
268    ///     id: &str,
269    ///     image_data: impl BufRead) -> Result<(), Error>
270    /// {
271    ///     const MAX_IMAGE_SIZE: usize = 4 * 1024 * 1024;
272    ///     let mut blob = BlobRead::with_upper_bound(image_data, MAX_IMAGE_SIZE);
273    ///
274    ///     let sql = "INSERT INTO Images (id, image_data) VALUES (?, ?)";
275    ///     let parameters = (&id.into_parameter(), &mut blob.as_blob_param());
276    ///     let timeout_sec = None;
277    ///     conn.execute(sql, parameters, timeout_sec)?;
278    ///     Ok(())
279    /// }
280    /// ```
281    pub fn with_upper_bound(buf_read: R, upper_bound: usize) -> Self {
282        Self {
283            exact: false,
284            consume: 0,
285            size: upper_bound,
286            buf_read,
287        }
288    }
289
290    /// Construct a blob read from any [`std::io::BufRead`]. The `upper bound` is used in the type
291    /// description then binding the blob as a parameter and is also passed to indicate the size
292    /// of the actual value to the ODBC driver.
293    ///
294    /// # Safety
295    ///
296    /// The ODBC driver may use the exact size hint to allocate buffers internally. Too short may
297    /// lead to invalid writes and too long may lead to invalid reads, so to be save the hint must
298    /// be exact.
299    pub unsafe fn with_exact_size(buf_read: R, exact_size: usize) -> Self {
300        Self {
301            exact: true,
302            consume: 0,
303            size: exact_size,
304            buf_read,
305        }
306    }
307}
308
309impl BlobRead<BufReader<File>> {
310    /// Construct a blob from a Path. The metadata of the file is used to give the ODBC driver a
311    /// size hint.
312    ///
313    /// # Example
314    ///
315    /// [`BlobRead::from_path`] is the most convenient way to turn a file path into a [`Blob`]
316    /// parameter. The following example also demonstrates that the streamed blob parameter can be
317    /// combined with reqular input parmeters like `id`.
318    ///
319    /// ```
320    /// use std::{error::Error, path::Path};
321    /// use odbc_api::{Connection, parameter::{Blob, BlobRead}, IntoParameter};
322    ///
323    /// fn insert_image_to_db(
324    ///     conn: &Connection<'_>,
325    ///     id: &str,
326    ///     image_path: &Path) -> Result<(), Box<dyn Error>>
327    /// {
328    ///     let mut blob = BlobRead::from_path(&image_path)?;
329    ///
330    ///     let sql = "INSERT INTO Images (id, image_data) VALUES (?, ?)";
331    ///     let parameters = (&id.into_parameter(), &mut blob.as_blob_param());
332    ///     let timeout_sec = None;
333    ///     conn.execute(sql, parameters, timeout_sec)?;
334    ///     Ok(())
335    /// }
336    /// ```
337    pub fn from_path(path: &Path) -> io::Result<Self> {
338        let file = File::open(path)?;
339        let size = file.metadata()?.len().try_into().unwrap();
340        let buf_read = BufReader::new(file);
341        Ok(Self {
342            consume: 0,
343            exact: true,
344            size,
345            buf_read,
346        })
347    }
348}
349
350impl<R> HasDataType for BlobRead<R>
351where
352    R: BufRead,
353{
354    fn data_type(&self) -> DataType {
355        DataType::LongVarbinary {
356            length: NonZeroUsize::new(self.size),
357        }
358    }
359}
360
361unsafe impl<R> Blob for BlobRead<R>
362where
363    R: BufRead,
364{
365    fn c_data_type(&self) -> CDataType {
366        CDataType::Binary
367    }
368
369    fn size_hint(&self) -> Option<usize> {
370        if self.exact { Some(self.size) } else { None }
371    }
372
373    fn next_batch(&mut self) -> io::Result<Option<&[u8]>> {
374        if self.consume != 0 {
375            self.buf_read.consume(self.consume);
376        }
377        let batch = self.buf_read.fill_buf()?;
378        self.consume = batch.len();
379        if batch.is_empty() {
380            Ok(None)
381        } else {
382            Ok(Some(batch))
383        }
384    }
385}