Skip to content

Add Blob I/O - #1083

Open
joriszwart wants to merge 25 commits into
mattn:masterfrom
joriszwart:master
Open

Add Blob I/O#1083
joriszwart wants to merge 25 commits into
mattn:masterfrom
joriszwart:master

Conversation

@joriszwart

Copy link
Copy Markdown

Incremental Blob I/O

This PR adds support for Incremental Blob I/O.

Notes

  • Modeled after backup using a driver connection.
  • The interfaces io.Reader and io.Closer have been implemented.
  • Related to issue Blob I/O #239

If this PR makes sense, I'll enhance it with additional io interfaces.

@joriszwart
joriszwart marked this pull request as ready for review September 1, 2022 14:39
@mattn

mattn commented Sep 1, 2022

Copy link
Copy Markdown
Owner

interesting.

@joriszwart
joriszwart marked this pull request as draft September 1, 2022 15:10
@joriszwart

joriszwart commented Sep 1, 2022

Copy link
Copy Markdown
Author

@joriszwart
joriszwart marked this pull request as ready for review September 1, 2022 15:32
Comment thread blob_io.go Outdated
Comment thread blob_io_test.go Outdated
Comment thread blob_io.go
Comment thread blob_io.go Outdated
Comment thread blob_io.go Outdated
@joriszwart

Copy link
Copy Markdown
Author

I have implemented write and seek support. As far as I'm concerned, this pull request is ready for further review. Thanks so far.

@joriszwart

joriszwart commented Sep 3, 2022

Copy link
Copy Markdown
Author

I was tempted to implement the io.ReaderAt and io.WriterAt interfaces as well, but clients can do that themselves by combining io.Reader (or io.Writer) and io.Seeker:

type Foo struct {
	io.ReadSeeker
}

func (f *Foo) ReadAt(p []byte, off int64) (int, error) {
	_, err := f.Seek(off, io.SeekStart)
	if err != nil {
		return 0, err
	}

	n, err := f.Read(p)
	return n, err
}

@joriszwart
joriszwart requested a review from rittneje September 3, 2022 18:50
Comment thread blob_io.go
Comment thread blob_io.go
Comment thread blob_io.go Outdated
Comment thread blob_io.go Outdated
Comment thread blob_io_test.go Outdated
@jlelse

jlelse commented Feb 3, 2023

Copy link
Copy Markdown

Any updates on this? It looks very interesting and enables "streaming" to and from the database! 👍

@joriszwart

Copy link
Copy Markdown
Author

@rittneje Is there anything I can do to get this approved?

Comment thread blob_io.go
Comment thread blob_io.go
Comment thread blob_io.go
n = len(b)
}

if n != len(b) {

@rittneje rittneje Mar 27, 2023

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Trying to remember - is there a reason not to do this check after the call to sqlite3_blob_write and only write what we can instead of nothing? I guess the current implementation is consistent with what sqlite3_blob_write internally does.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@joriszwart following up on this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I don't know what to do. Sorry.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@rittneje can you help me out?

@gabriel-samfira gabriel-samfira Sep 27, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This function implements io.Writer{}, which states:

https://pkg.go.dev/io#Writer

Write writes len(p) bytes from p to the underlying data stream. It returns the number of bytes written from p (0 <= n <= len(p)) and any error encountered that caused the write to stop early. Write must return a non-nil error if it returns n < len(p). Write must not modify the slice data, even temporarily.

The interesting bit of this is:

Write must return a non-nil error if it returns n < len(p).

So if b is partially written, then n should return the number of bytes written and a non nil error. But to be honest, the way it's written now, should be fine. I mean, partial writes are worse than no writes. At least the caller knows it erred and can retry using the same byte slice.

At least that's what I understand from the io.Writer docs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The only thing that would be nice to have is maybe an error that denotes not enough space. Something like ENOSPC on linux and ERROR_HANDLE_DISK_FULL on Windows (or if sqlite already has an error code for not enough space, to use that). But that's just a nit. It would help the caller when checking with errors.Is(err, NotEnoughSpacePlatformSpecificError).

I think sqlite has SQLITE_FULL and SQLITE_IOERR_DISKFULL.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

partial writes are worse than no writes

This is a subjective statement.

At least the caller knows it erred and can retry using the same byte slice.

If the error is because the blob is full, retrying is never going to work.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You're right on both counts.

@rittneje

Copy link
Copy Markdown
Collaborator

@joriszwart I am away from my dev machine or I'd just add those two len checks myself. They should be pretty simple to add.

@joriszwart

Copy link
Copy Markdown
Author

@joriszwart I am away from my dev machine or I'd just add those two len checks myself. They should be pretty simple to add.

Can you add them?

@joriszwart

joriszwart commented Aug 21, 2023

Copy link
Copy Markdown
Author

Anyone else? @graf0 @pokstad @jlelse @lezhnev74 @mitar?

@joriszwart
joriszwart requested a review from rittneje October 8, 2023 18:26
Comment thread blob_io_test.go Outdated
Comment thread blob_io_test.go
if err != nil {
t.Fatal(err)
}
defer driverConn.Close()

@rittneje rittneje Oct 22, 2023

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove. This is superfluous with the call to conn.Close() above. (And also you aren't supposed to do anything with the conn outside the Raw callback.)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Remove those 4 lines? Or only the last?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Remove only the last line?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think just line 72. The driverConn is just conn cast as the underlying type that implements the interface (conn). Calling Close() on conn will close driverConn as well.

@joriszwart

Copy link
Copy Markdown
Author

@rittneje can I leave the suggested changes to you? That would make this process more efficient.

@joriszwart
joriszwart requested a review from rittneje November 13, 2023 09:01
jrossi added a commit to jrossi/go-sqlite3 that referenced this pull request Mar 27, 2024
Pulling in this RP for Blob access
@joriszwart joriszwart closed this May 24, 2024
@gabriel-samfira

Copy link
Copy Markdown

A shame this was abandoned. Thanks for trying @joriszwart !

@joriszwart

Copy link
Copy Markdown
Author

@mattn @rittneje any interest in this?

@joriszwart joriszwart reopened this Sep 26, 2025
@gabriel-samfira

Copy link
Copy Markdown

@mattn @rittneje any interest in this?

for what it's worth, I would love to see this happen.

Comment thread blob_io.go

// Write implements the io.Writer interface.
func (s *SQLiteBlob) Write(b []byte) (n int, err error) {
if len(b) == 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I recommend you create a copy of b and use that throughout. Slices may be mutated after passed to Write(). You might end up writing the first few bytes from the initial data and the rest from the new set when b is overwritten by the caller. In go only the slice header is passed by value. The underlying array is shared with the caller.

Something like:

tmp := make([]byte, len(b))
copy(tmp, b)

Then use tmp instead of b.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Doesn't making a copy defeat the purpose of streaming blobs?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not really, as you'd only be copying the buffer (which is small - usually around 1024 bytes). But generally speaking you'd only have to copy if you need to guard against caller reuse of the buffer. This is usually done in logging writers that may want to return control back to the caller while the write operation against the logging backend is still ongoing.

I'm not sure that would apply here though (given that we want to wait for the write to happen and only then return), so I just added a comment as a "recommendation". Feel free to ignore me.

I haven't mentioned this, but thanks for reopening this PR!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There is no need to make a copy of the slice, as sqlite3_blob_write is going to copy the data. And the caller is not allowed to modify the slice during the call the Write. (Even if they did, copy itself would be subject to a race condition anyway.)

@gabriel-samfira

Copy link
Copy Markdown

for what it's worth, I rebased this PR on top of main and I'm using it in one of my projects. It works great. This actively makes my life easier, as I don't need to add an additional requirement for users to configure a new writable path for small (very limited in number and size) files that the app requires to function properly. So thanks @joriszwart for this PR.

@joriszwart

Copy link
Copy Markdown
Author

So, how to proceed?

@mattn

mattn commented Mar 16, 2026

Copy link
Copy Markdown
Owner

Thank you for this PR. The feature itself is useful and we'd like to get it merged. However, there are a few things that need to be addressed first:

  • Needs a rebase onto current master (the base is from 2022)
  • The len checks mentioned by @rittneje are still missing
  • lastError() signature has changed — calls need to be updated
  • The test uses :memory: with SetMaxOpenConns(1) — please use file:/<name>?vfs=memdb instead

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.

7 participants