Skip to content

Implement BLOB reading for sqlite#4994

Open
matthiasbeyer wants to merge 5 commits intodiesel-rs:mainfrom
matthiasbeyer:sqlite-blob-reading
Open

Implement BLOB reading for sqlite#4994
matthiasbeyer wants to merge 5 commits intodiesel-rs:mainfrom
matthiasbeyer:sqlite-blob-reading

Conversation

@matthiasbeyer
Copy link

This patch implements BLOB reading via the sqlite3_blob_read() API.

--

We (@weiznich and I) talked about this on mastodon.

This is a first proposal to see what you think of it.

This patch would not have been possible without the tremendous help from @TheNeikos who is mentioned as a co-author in the patch. Thank you very much for your help ❤️

@matthiasbeyer matthiasbeyer force-pushed the sqlite-blob-reading branch 3 times, most recently from 8d1cc15 to 90e35aa Compare March 2, 2026 10:01
NonZeroI64::new(self.raw_connection.last_insert_rowid())
}

pub fn get_blob<'conn, 'query, U>(&'conn self, primary_key: i64) -> Result<sqlite_blob::SqliteBlob<'conn>, Error>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation missing here, yes.

This patch implements BLOB reading via the sqlite3_blob_read() API.

Co-authored-by: Marcel Müller <neikos@neikos.email>
Signed-off-by: Marcel Müller <neikos@neikos.email>
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
@weiznich weiznich requested a review from a team March 2, 2026 13:40
@weiznich
Copy link
Member

weiznich commented Mar 2, 2026

Thanks for working on this. I (or someone else) will try to have a look at the implementation in the next days/weeks.

cc @LucaCappelletti94 as he has done quite a lot of Sqlite api additions in the last weeks and might also help here

@TheNeikos
Copy link

TheNeikos commented Mar 2, 2026

One thing I'd like to mention wrt. Blobs in SQLITE and the current interface is that one cannot hold Blobs while executing any other command.

This is fine and the easiest way from a technical standpoint. However I feel it does preclude from more interesting applications like having proper-streaming or similar backed by a sqlite database. The docs seem fairly clear on this:

If the database connection is associated with unfinalized prepared statements, BLOB handlers, and/or unfinished sqlite3_backup objects then sqlite3_close() will leave the database connection open and return SQLITE_BUSY.
(Emphasis mine)

As well as in blob_open:

If the row that a BLOB handle points to is modified by an UPDATE, DELETE, or by ON CONFLICT side-effects then the BLOB handle is marked as "expired". This is true if any column of the row is changed, even a column other than the one the BLOB handle is open on. Calls to sqlite3_blob_read() and sqlite3_blob_write() for an expired BLOB handle fail with a return code of SQLITE_ABORT. Changes written into a BLOB prior to the BLOB expiring are not rolled back by the expiration of the BLOB. Such changes will eventually commit if the transaction continues to completion.

This means that the lifetime of a Blob is tied to a database, but not in a way that Rust can properly check. The current interface also means that users cannot move a blob handle from where it was created (due to blob being bound to the lifetime of the connection).

As far as I can tell it is safe to not have the lifetime. However, this would introduce the hazard of panicking when dropping the SQLite connection while having any blobs open (as it would return SQLITE_BUSY on close).

None of these are real 'problems' IMO, just decisions to be made one way or another.

I believe a potential path forward could be something akin to a detach method on the blob that would make it 'static and put the onus on the user to understand what this entails.

Copy link
Member

@weiznich weiznich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. Overall this looks like a good start already. I've left a bunch of comments around the API design I would like to discuss and potentially address before merging.

I still need to look a the tests to decide if we want to add more cases.

Finally I think that's a feature that deserves a changelog entry, so please at it to the top level Changelog.md file.

}
}

impl SqliteBlob<'_> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We likely also want to implement Write?

That doesn't necessarily need to be done in this pr, but the design should allow it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would like to include that later down the road!

table_name.as_c_str().as_ptr(),
column_name.as_c_str().as_ptr(),
pkey,
0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a parameter to control this flag. Maybe even as compile time flag (generic parameter?)

Otherwise an enum could also be fine. For now that could just have a read variant and be marked as #[non_exhaustive]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean here. If you don't mind, I would include Write functionality in this very same PR later down the road and do that bit of refactoring for a generic parameter (or something like that) then.

But first I would like to get the Read functionality in a shape that's ACK'd by you or other diesel maintainers!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to also postpone the write functionality to a later PR.

This comment was mostly about API design.

@matthiasbeyer
Copy link
Author

I pushed a fixup to address a number of your review points, where I did not have any questions! Also included some SAFETY comments in the codebase now.

NonZeroI64::new(self.raw_connection.last_insert_rowid())
}

pub fn get_blob<'conn, 'query, U>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undocumented public method, maybe a documentation with doctests would be nice to have?

Copy link
Member

@weiznich weiznich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update and sorry for taking that long to review it :(

The API design now mostly looks good to me, beside the minor note about close. I would be happy to merge this with the remaining minor things resolved, a bit of documentation + a changelog entry added.

ffi::code_to_str(err_code)
}

#[doc(hidden)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: I usually put these traits in a local mod private {} and then reexport them as crate local via pub(crate) use self::private::HasDatabaseAndTableName; in the parent scope. That ensures that they are really not usable by downstream crates.

table_name.as_c_str().as_ptr(),
column_name.as_c_str().as_ptr(),
pkey,
0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to also postpone the write functionality to a later PR.

This comment was mostly about API design.

Comment on lines +45 to +46
/// Using the handle after calling this function is an error if not otherwise stated.
pub fn close(&mut self) -> Result<(), crate::result::Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rather expose this publically as consuming method (so pub fn clone(self)) and only internally have a fn close_intern(&mut self) method that can be shared between Drop and this function?

Seems like an easy way to remove this potential error path

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants