diff --git a/README.md b/README.md index a1863af..97799a4 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ and WAL databases are not supported. - [x] file locking, compatible with SQLite on macOS/Linux/Windows - [ ] advanced SQLite features - [x] nested transactions - - [ ] incremental BLOB I/O + - [x] incremental BLOB I/O - [ ] online backup - [ ] snapshots - [ ] session extension diff --git a/blob.go b/blob.go index 739b25f..f505d69 100644 --- a/blob.go +++ b/blob.go @@ -1,6 +1,146 @@ package sqlite3 +import "io" + // ZeroBlob represents a zero-filled, length n BLOB // that can be used as an argument to // [database/sql.DB.Exec] and similar methods. type ZeroBlob int64 + +// Blob is a handle to an open BLOB. +// +// https://www.sqlite.org/c3ref/blob.html +type Blob struct { + c *Conn + handle uint32 + bytes int64 + offset int64 +} + +var _ io.ReadWriteSeeker = &Blob{} + +// OpenBlob opens a BLOB for incremental I/O. +// +// https://www.sqlite.org/c3ref/blob_open.html +func (c *Conn) OpenBlob(db, table, column string, row uint64, write bool) (*Blob, error) { + defer c.arena.reset() + blobPtr := c.arena.new(ptrlen) + dbPtr := c.arena.string(db) + tablePtr := c.arena.string(table) + columnPtr := c.arena.string(column) + + var flags uint64 + if write { + flags = 1 + } + + r := c.call(c.api.blobOpen, uint64(c.handle), + uint64(dbPtr), uint64(tablePtr), uint64(columnPtr), + row, flags, uint64(blobPtr)) + + if err := c.error(r[0]); err != nil { + return nil, err + } + + blob := Blob{c: c} + blob.handle = c.mem.readUint32(blobPtr) + blob.bytes = int64(c.call(c.api.blobBytes, uint64(blob.handle))[0]) + return &blob, nil +} + +// Close closes a BLOB handle. +// +// It is safe to close a nil, zero or closed BLOB handle. +// +// https://www.sqlite.org/c3ref/blob_close.html +func (b *Blob) Close() error { + if b == nil || b.handle == 0 { + return nil + } + + r := b.c.call(b.c.api.blobClose, uint64(b.handle)) + + b.handle = 0 + return b.c.error(r[0]) +} + +// Size returns the size of the BLOB in bytes. +// +// https://www.sqlite.org/c3ref/blob_bytes.html +func (b *Blob) Size() int64 { + return b.bytes +} + +// Read implements the [io.Reader] interface. +// +// https://www.sqlite.org/c3ref/blob_read.html +func (b *Blob) Read(p []byte) (n int, err error) { + if b.offset >= b.bytes { + return 0, io.EOF + } + + want := int64(len(p)) + avail := b.bytes - b.offset + if want > avail { + want = avail + } + + ptr := b.c.new(uint64(want)) + defer b.c.free(ptr) + + r := b.c.call(b.c.api.blobRead, uint64(b.handle), + uint64(ptr), uint64(want), uint64(b.offset)) + err = b.c.error(r[0]) + if err != nil { + return 0, err + } + + mem := b.c.mem.view(ptr, uint64(want)) + copy(p, mem) + b.offset += want + if b.offset >= b.bytes { + err = io.EOF + } + return int(want), err +} + +// Write implements the [io.Writer] interface. +// +// https://www.sqlite.org/c3ref/blob_write.html +func (b *Blob) Write(p []byte) (n int, err error) { + offset := b.offset + if offset > b.bytes { + offset = b.bytes + } + + ptr := b.c.newBytes(p) + defer b.c.free(ptr) + + r := b.c.call(b.c.api.blobWrite, uint64(b.handle), + uint64(ptr), uint64(len(p)), uint64(offset)) + err = b.c.error(r[0]) + if err != nil { + return 0, err + } + b.offset += int64(len(p)) + return len(p), nil +} + +// Seek implements the [io.Seeker] interface. +func (b *Blob) Seek(offset int64, whence int) (int64, error) { + switch whence { + default: + return 0, whenceErr + case io.SeekStart: + break + case io.SeekCurrent: + offset += b.offset + case io.SeekEnd: + offset += b.bytes + } + if offset < 0 { + return 0, offsetErr + } + b.offset = offset + return offset, nil +} diff --git a/error.go b/error.go index 1c96733..d3a1cbd 100644 --- a/error.go +++ b/error.go @@ -203,6 +203,8 @@ const ( timeErr = errorString("sqlite3: invalid time value") tailErr = errorString("sqlite3: non-empty tail") notImplErr = errorString("sqlite3: not implemented") + whenceErr = errorString("sqlite3: invalid whence") + offsetErr = errorString("sqlite3: invalid offset") ) func assertErr() errorString {