Module rusqlite::blob

source ·
Available on crate feature blob only.
Expand description

Incremental BLOB I/O.

Note that SQLite does not provide API-level access to change the size of a BLOB; that must be performed through SQL statements.

There are two choices for how to perform IO on a Blob.

  1. The implementations it provides of the std::io::Read, std::io::Write, and std::io::Seek traits.

  2. A positional IO API, e.g. Blob::read_at, Blob::write_at and similar.

Documenting these in order:

1. std::io trait implementations.

Blob conforms to std::io::Read, std::io::Write, and std::io::Seek, so it plays nicely with other types that build on these (such as std::io::BufReader and std::io::BufWriter). However, you must be careful with the size of the blob. For example, when using a BufWriter, the BufWriter will accept more data than the Blob will allow, so make sure to call flush and check for errors. (See the unit tests in this module for an example.)

2. Positional IO

Blobs also offer a pread / pwrite-style positional IO api in the form of Blob::read_at, Blob::write_at, Blob::raw_read_at, Blob::read_at_exact, and Blob::raw_read_at_exact.

These APIs all take the position to read from or write to from as a parameter, instead of using an internal pos value.

Positional IO Read Variants

For the read functions, there are several functions provided:

These can be divided along two axes: raw/not raw, and exact/inexact:

  1. Raw/not raw refers to the type of the destination buffer. The raw functions take a &mut [MaybeUninit<u8>] as the destination buffer, where the “normal” functions take a &mut [u8].

    Using MaybeUninit here can be more efficient in some cases, but is often inconvenient, so both are provided.

  2. Exact/inexact refers to to whether or not the entire buffer must be filled in order for the call to be considered a success.

    The “exact” functions require the provided buffer be entirely filled, or they return an error, whereas the “inexact” functions read as much out of the blob as is available, and return how much they were able to read.

    The inexact functions are preferable if you do not know the size of the blob already, and the exact functions are preferable if you do.

Comparison to using the std::io traits:

In general, the positional methods offer the following Pro/Cons compared to using the implementation std::io::{Read, Write, Seek} we provide for Blob:

  1. (Pro) There is no need to first seek to a position in order to perform IO on it as the position is a parameter.

  2. (Pro) Blob’s positional read functions don’t mutate the blob in any way, and take &self. No &mut access required.

  3. (Pro) Positional IO functions return Err(rusqlite::Error) on failure, rather than Err(std::io::Error). Returning rusqlite::Error is more accurate and convenient.

    Note that for the std::io API, no data is lost however, and it can be recovered with io_err.downcast::<rusqlite::Error>() (this can be easy to forget, though).

  4. (Pro, for now). A raw version of the read API exists which can allow reading into a &mut [MaybeUninit<u8>] buffer, which avoids a potential costly initialization step. (However, std::io traits will certainly gain this someday, which is why this is only a “Pro, for now”).

  5. (Con) The set of functions is more bare-bones than what is offered in std::io, which has a number of adapters, handy algorithms, further traits.

  6. (Con) No meaningful interoperability with other crates, so if you need that you must use std::io.

To generalize: the std::io traits are useful because they conform to a standard interface that a lot of code knows how to handle, however that interface is not a perfect fit for Blob, so another small set of functions is provided as well.

Example (std::io)

let db = Connection::open_in_memory()?;
db.execute_batch("CREATE TABLE test_table (content BLOB);")?;

// Insert a BLOB into the `content` column of `test_table`. Note that the Blob
// I/O API provides no way of inserting or resizing BLOBs in the DB -- this
// must be done via SQL.
db.execute("INSERT INTO test_table (content) VALUES (ZEROBLOB(10))", [])?;

// Get the row id off the BLOB we just inserted.
let rowid = db.last_insert_rowid();
// Open the BLOB we just inserted for IO.
let mut blob = db.blob_open(DatabaseName::Main, "test_table", "content", rowid, false)?;

// Write some data into the blob. Make sure to test that the number of bytes
// written matches what you expect; if you try to write too much, the data
// will be truncated to the size of the BLOB.
let bytes_written = blob.write(b"01234567")?;
assert_eq!(bytes_written, 8);

// Move back to the start and read into a local buffer.
// Same guidance - make sure you check the number of bytes read!
blob.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; 20];
let bytes_read = blob.read(&mut buf[..])?;
assert_eq!(bytes_read, 10); // note we read 10 bytes because the blob has size 10

// Insert another BLOB, this time using a parameter passed in from
// rust (potentially with a dynamic size).
db.execute(
    "INSERT INTO test_table (content) VALUES (?1)",
    [ZeroBlob(64)],
)?;

// given a new row ID, we can reopen the blob on that row
let rowid = db.last_insert_rowid();
blob.reopen(rowid)?;
// Just check that the size is right.
assert_eq!(blob.len(), 64);

Example (Positional)

let db = Connection::open_in_memory()?;
db.execute_batch("CREATE TABLE test_table (content BLOB);")?;
// Insert a blob into the `content` column of `test_table`. Note that the Blob
// I/O API provides no way of inserting or resizing blobs in the DB -- this
// must be done via SQL.
db.execute("INSERT INTO test_table (content) VALUES (ZEROBLOB(10))", [])?;
// Get the row id off the blob we just inserted.
let rowid = db.last_insert_rowid();
// Open the blob we just inserted for IO.
let mut blob = db.blob_open(DatabaseName::Main, "test_table", "content", rowid, false)?;
// Write some data into the blob.
blob.write_at(b"ABCDEF", 2)?;

// Read the whole blob into a local buffer.
let mut buf = [0u8; 10];
blob.read_at_exact(&mut buf, 0)?;
assert_eq!(&buf, b"\0\0ABCDEF\0\0");

// Insert another blob, this time using a parameter passed in from
// rust (potentially with a dynamic size).
db.execute(
    "INSERT INTO test_table (content) VALUES (?1)",
    [ZeroBlob(64)],
)?;

// given a new row ID, we can reopen the blob on that row
let rowid = db.last_insert_rowid();
blob.reopen(rowid)?;
assert_eq!(blob.len(), 64);

Structs

  • Handle to an open BLOB. See rusqlite::blob documentation for in-depth discussion.
  • BLOB of length N that is filled with zeroes.