Skip to content

Commit 01dbf4b

Browse files
committed
Blob I/O: add write and seek
1 parent dff49f9 commit 01dbf4b

File tree

2 files changed

+194
-3
lines changed

2 files changed

+194
-3
lines changed

blob_io.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ package sqlite3
1616
import "C"
1717

1818
import (
19+
"errors"
1920
"io"
21+
"math"
2022
"runtime"
2123
"unsafe"
2224
)
@@ -33,7 +35,7 @@ type SQLiteBlob struct {
3335
//
3436
// See https://www.sqlite.org/c3ref/blob_open.html for usage.
3537
//
36-
// Should only be used with conn.Raw. The flag parameter is ignored.
38+
// Should only be used with conn.Raw.
3739
func (conn *SQLiteConn) Blob(database, table, column string, rowid int64, flags int) (*SQLiteBlob, error) {
3840
databaseptr := C.CString(database)
3941
defer C.free(unsafe.Pointer(databaseptr))
@@ -81,6 +83,55 @@ func (s *SQLiteBlob) Read(b []byte) (n int, err error) {
8183
return n, nil
8284
}
8385

86+
// Write implements the io.Writer interface.
87+
func (s *SQLiteBlob) Write(b []byte) (n int, err error) {
88+
if s.offset >= s.size {
89+
return 0, io.EOF
90+
}
91+
92+
n = s.size - s.offset
93+
if len(b) < n {
94+
n = len(b)
95+
}
96+
97+
p := &b[0]
98+
ret := C.sqlite3_blob_write(s.blob, unsafe.Pointer(p), C.int(n), C.int(s.offset))
99+
if ret != C.SQLITE_OK {
100+
return 0, s.conn.lastError()
101+
}
102+
103+
s.offset += n
104+
105+
return n, nil
106+
}
107+
108+
// Seek implements the io.Seeker interface.
109+
func (s *SQLiteBlob) Seek(offset int64, whence int) (int64, error) {
110+
if offset > math.MaxInt32 {
111+
return 0, errors.New("sqlite3.SQLiteBlob.Seek: invalid position")
112+
}
113+
114+
var abs int64
115+
switch whence {
116+
case io.SeekStart:
117+
abs = offset
118+
case io.SeekCurrent:
119+
abs = int64(s.offset) + offset
120+
case io.SeekEnd:
121+
abs = int64(s.size) + offset
122+
default:
123+
return 0, errors.New("sqlite3.SQLiteBlob.Seek: invalid whence")
124+
}
125+
126+
if abs < 0 {
127+
return 0, errors.New("sqlite.SQLiteBlob.Seek: negative position")
128+
}
129+
130+
s.offset = int(abs)
131+
132+
return abs, nil
133+
}
134+
84135
// Close implements the io.Closer interface.
85136
func (s *SQLiteBlob) Close() error {
86137
ret := C.sqlite3_blob_close(s.blob)

blob_io_test.go

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import (
1616
"testing"
1717
)
1818

19+
// Verify interface implementations
20+
var _ io.Reader = &SQLiteBlob{}
21+
var _ io.Writer = &SQLiteBlob{}
22+
var _ io.Seeker = &SQLiteBlob{}
23+
var _ io.Closer = &SQLiteBlob{}
24+
1925
func blobTestData(dbname string, rowid int64, blob []byte) (*sql.DB, *SQLiteConn, error) {
2026
db, err := sql.Open("sqlite3", "file:"+dbname+"?mode=memory&cache=shared")
2127
if err != nil {
@@ -54,11 +60,11 @@ func blobTestData(dbname string, rowid int64, blob []byte) (*sql.DB, *SQLiteConn
5460
return db, driverConn, nil
5561
}
5662

57-
func TestBlobIO(t *testing.T) {
63+
func TestBlobRead(t *testing.T) {
5864
rowid := int64(6581)
5965
expected := []byte("I ❤️ SQLite in \x00\x01\x02…")
6066

61-
db, driverConn, err := blobTestData("testblobio", rowid, expected)
67+
db, driverConn, err := blobTestData("testblobread", rowid, expected)
6268
if err != nil {
6369
t.Fatal("Failed to get raw connection:", err)
6470
}
@@ -109,3 +115,137 @@ func TestBlobIO(t *testing.T) {
109115
t.Error("Expected EOF", err)
110116
}
111117
}
118+
119+
func TestBlobWrite(t *testing.T) {
120+
rowid := int64(8580)
121+
expected := []byte{
122+
// Random data from /dev/urandom
123+
0xe5, 0x48, 0x94, 0xad, 0xa6, 0x7c, 0x81, 0xa2, 0x70, 0x07, 0x79, 0x60,
124+
0x33, 0xbc, 0x64, 0x33, 0x8f, 0x48, 0x43, 0xa6, 0x33, 0x5c, 0x08, 0x32,
125+
}
126+
127+
// Allocate a zero blob
128+
data := make([]byte, len(expected))
129+
db, driverConn, err := blobTestData("testblobwrite", rowid, data)
130+
if err != nil {
131+
t.Fatal("Failed to get raw connection:", err)
132+
}
133+
defer driverConn.Close()
134+
defer db.Close()
135+
136+
// Open blob for read/write
137+
blob, err := driverConn.Blob("main", "data", "value", rowid, 1)
138+
if err != nil {
139+
t.Error("failed", err)
140+
}
141+
defer blob.Close()
142+
143+
// Write blob incrementally
144+
middle := len(expected) / 2
145+
first := expected[:middle]
146+
second := expected[middle:]
147+
148+
// Write part Ⅰ
149+
n1, err := blob.Write(first)
150+
151+
if err != nil || n1 != len(first) {
152+
t.Errorf("Failed to write %d bytes", n1)
153+
}
154+
155+
// Write part Ⅱ
156+
n2, err := blob.Write(second)
157+
158+
if err != nil || n2 != len(second) {
159+
t.Errorf("Failed to write %d bytes", n2)
160+
}
161+
162+
// EOF
163+
b3 := make([]byte, 10)
164+
n3, err := blob.Write(b3)
165+
166+
if err != io.EOF || n3 != 0 {
167+
t.Error("Expected EOF", err)
168+
}
169+
170+
// Verify written data
171+
_, err = blob.Seek(0, io.SeekStart)
172+
if err != nil {
173+
t.Fatal("Failed to seek:", err)
174+
}
175+
176+
b4 := make([]byte, len(expected))
177+
n4, err := blob.Read(b4)
178+
179+
if err != nil || n4 != len(b4) {
180+
t.Errorf("Failed to read %d bytes", n4)
181+
}
182+
183+
if bytes.Compare(expected, b4) != 0 {
184+
t.Error("Expected\n", expected, "got\n", b4)
185+
}
186+
}
187+
188+
func TestBlobSeek(t *testing.T) {
189+
rowid := int64(6510)
190+
data := make([]byte, 1000)
191+
192+
db, driverConn, err := blobTestData("testblobseek", rowid, data)
193+
if err != nil {
194+
t.Fatal("Failed to get raw connection:", err)
195+
}
196+
defer driverConn.Close()
197+
defer db.Close()
198+
199+
// Open blob
200+
blob, err := driverConn.Blob("main", "data", "value", rowid, 0)
201+
if err != nil {
202+
t.Error("failed", err)
203+
}
204+
defer blob.Close()
205+
206+
// Test data
207+
begin := int64(0)
208+
middle := int64(len(data) / 2)
209+
end := int64(len(data) - 1)
210+
eof := int64(len(data))
211+
212+
tests := []struct {
213+
offset int64
214+
whence int
215+
expected int64
216+
}{
217+
{offset: begin, whence: io.SeekStart, expected: begin},
218+
{offset: middle, whence: io.SeekStart, expected: middle},
219+
{offset: end, whence: io.SeekStart, expected: end},
220+
{offset: eof, whence: io.SeekStart, expected: eof},
221+
222+
{offset: -1, whence: io.SeekCurrent, expected: middle - 1},
223+
{offset: 0, whence: io.SeekCurrent, expected: middle},
224+
{offset: 1, whence: io.SeekCurrent, expected: middle + 1},
225+
{offset: -middle, whence: io.SeekCurrent, expected: begin},
226+
227+
{offset: -2, whence: io.SeekEnd, expected: end - 1},
228+
{offset: -1, whence: io.SeekEnd, expected: end},
229+
{offset: 0, whence: io.SeekEnd, expected: eof},
230+
{offset: 1, whence: io.SeekEnd, expected: eof + 1},
231+
{offset: -eof, whence: io.SeekEnd, expected: begin},
232+
}
233+
234+
for _, tc := range tests {
235+
// Start in the middle
236+
_, err := blob.Seek(middle, io.SeekStart)
237+
if err != nil {
238+
t.Fatal("Failed to seek:", err)
239+
}
240+
241+
// Test
242+
got, err := blob.Seek(tc.offset, tc.whence)
243+
if err != nil {
244+
t.Fatal("Failed to seek:", err)
245+
}
246+
247+
if tc.expected != got {
248+
t.Error("For", tc, "expected", tc.expected, "got", got)
249+
}
250+
}
251+
}

0 commit comments

Comments
 (0)