From c357e0c875410b5ea3255fc54ed5534f01ed0563 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Thu, 1 Sep 2022 16:16:27 +0200 Subject: [PATCH 01/20] Add Blob I/O --- blob_io.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ blob_io_test.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 blob_io.go create mode 100644 blob_io_test.go diff --git a/blob_io.go b/blob_io.go new file mode 100644 index 00000000..135fd20b --- /dev/null +++ b/blob_io.go @@ -0,0 +1,94 @@ +// Copyright (C) 2022 Yasuhiro Matsumoto . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package sqlite3 + +/* +#ifndef USE_LIBSQLITE3 +#include "sqlite3-binding.h" +#else +#include +#endif +#include +*/ +import "C" + +import ( + "io" + "runtime" + "unsafe" +) + +// SQLiteBlob implements the SQLite Blob I/O interface. +type SQLiteBlob struct { + conn *SQLiteConn + blob *C.sqlite3_blob + size int + offs int +} + +// Blob opens a blob. +// +// The flag parameter is ignored. +func (conn *SQLiteConn) Blob(database, table, column string, rowid int64, flags int) (*SQLiteBlob, error) { + databaseptr := C.CString(database) + defer C.free(unsafe.Pointer(databaseptr)) + + tableptr := C.CString(table) + defer C.free(unsafe.Pointer(tableptr)) + + columnptr := C.CString(column) + defer C.free(unsafe.Pointer(columnptr)) + + var blob *C.sqlite3_blob + ret := C.sqlite3_blob_open(conn.db, databaseptr, tableptr, columnptr, C.longlong(rowid), C.int(flags), &blob) + + if ret == C.SQLITE_OK { + size := int(C.sqlite3_blob_bytes(blob)) + bb := &SQLiteBlob{conn, blob, size, 0} + + runtime.SetFinalizer(bb, (*SQLiteBlob).Close) + + return bb, nil + } + + return nil, conn.lastError() +} + +// Read implements the io.Reader interface. +func (s *SQLiteBlob) Read(b []byte) (n int, err error) { + if s.offs >= s.size { + return 0, io.EOF + } + + n = s.size - s.offs + if len(b) < n { + n = len(b) + } + + p := &b[0] + ret := C.sqlite3_blob_read(s.blob, unsafe.Pointer(p), C.int(n), C.int(s.offs)) + if ret != C.SQLITE_OK { + return 0, s.conn.lastError() + } + + s.offs += n + + return n, nil +} + +// Close implements the io.Closer interface. +func (s *SQLiteBlob) Close() error { + ret := C.sqlite3_blob_close(s.blob) + + s.blob = nil + runtime.SetFinalizer(s, nil) + + if ret != C.SQLITE_OK { + return s.conn.lastError() + } + + return nil +} diff --git a/blob_io_test.go b/blob_io_test.go new file mode 100644 index 00000000..cf17cfe3 --- /dev/null +++ b/blob_io_test.go @@ -0,0 +1,94 @@ +// Copyright (C) 2022 Yasuhiro Matsumoto . +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +//go:build cgo +// +build cgo + +package sqlite3 + +import ( + "bytes" + "database/sql" + "io" + "testing" +) + +func TestBlobIO(t *testing.T) { + driverName := "sqlite3_TestBlobIO2" + var driverConn *SQLiteConn + sql.Register(driverName, &SQLiteDriver{ + ConnectHook: func(conn *SQLiteConn) error { + driverConn = conn + return nil + }}) + + db, err := sql.Open(driverName, ":memory:") + if err != nil { + t.Fatal("Fail to open:", err) + } + defer db.Close() + + // Test data + expected := []byte("I ❤️ SQLite in \x00\x01\x02…") + rowid := int64(6581) + + query := ` + CREATE TABLE data ( + value BLOB + ); + + INSERT INTO data (_rowid_, value) + VALUES (:rowid, :value); + ` + + _, err = db.Exec(query, sql.Named("rowid", rowid), sql.Named("value", expected)) + if err != nil { + t.Fatal("Failed to execute", err) + } + + // Open blob + blob, err := driverConn.Blob("main", "data", "value", rowid, 0) + if err != nil { + t.Error("failed", err) + } + defer blob.Close() + + // Read blob incrementally + middle := len(expected) / 2 + first := expected[:middle] + second := expected[middle:] + + // Read part Ⅰ + b1 := make([]byte, len(first)) + n1, err := blob.Read(b1) + + if err != nil || n1 != len(b1) { + t.Errorf("Failed to read %d bytes", n1) + } + + if bytes.Compare(first, b1) != 0 { + t.Error("Expected\n", first, "got\n", b1) + } + + // Read part Ⅱ + b2 := make([]byte, len(second)) + n2, err := blob.Read(b2) + + if err != nil || n2 != len(b2) { + t.Errorf("Failed to read %d bytes", n2) + } + + if bytes.Compare(second, b2) != 0 { + t.Error("Expected\n", second, "got\n", b2) + } + + // EOF + b3 := make([]byte, 10) + n3, err := blob.Read(b3) + + if err != io.EOF || n3 != 0 { + t.Error("Expected EOF", err) + } +} From 88d6cfdf57a1d21c61554b3fb9b59ca21887a284 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 2 Sep 2022 09:26:47 +0200 Subject: [PATCH 02/20] Blob I/O: early return --- blob_io.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blob_io.go b/blob_io.go index 135fd20b..e573273c 100644 --- a/blob_io.go +++ b/blob_io.go @@ -45,16 +45,16 @@ func (conn *SQLiteConn) Blob(database, table, column string, rowid int64, flags var blob *C.sqlite3_blob ret := C.sqlite3_blob_open(conn.db, databaseptr, tableptr, columnptr, C.longlong(rowid), C.int(flags), &blob) - if ret == C.SQLITE_OK { - size := int(C.sqlite3_blob_bytes(blob)) - bb := &SQLiteBlob{conn, blob, size, 0} + if ret != C.SQLITE_OK { + return nil, conn.lastError() + } - runtime.SetFinalizer(bb, (*SQLiteBlob).Close) + size := int(C.sqlite3_blob_bytes(blob)) + bb := &SQLiteBlob{conn, blob, size, 0} - return bb, nil - } + runtime.SetFinalizer(bb, (*SQLiteBlob).Close) - return nil, conn.lastError() + return bb, nil } // Read implements the io.Reader interface. From c6a9868c6eb012aad836702a278ae3c4ad913013 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 2 Sep 2022 09:27:17 +0200 Subject: [PATCH 03/20] Blob I/O: use raw connection --- blob_io_test.go | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/blob_io_test.go b/blob_io_test.go index cf17cfe3..a33ae1e9 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -10,26 +10,34 @@ package sqlite3 import ( "bytes" + "context" "database/sql" "io" "testing" ) func TestBlobIO(t *testing.T) { - driverName := "sqlite3_TestBlobIO2" - var driverConn *SQLiteConn - sql.Register(driverName, &SQLiteDriver{ - ConnectHook: func(conn *SQLiteConn) error { - driverConn = conn - return nil - }}) - - db, err := sql.Open(driverName, ":memory:") + db, err := sql.Open("sqlite3", "file:testblobio?mode=memory&cache=shared") if err != nil { t.Fatal("Fail to open:", err) } defer db.Close() + conn, err := db.Conn(context.Background()) + if err != nil { + t.Fatal("Failed to get raw connection:", err) + } + defer conn.Close() + + var driverConn *SQLiteConn + err = conn.Raw(func(conn interface{}) error { + driverConn = conn.(*SQLiteConn) + return nil + }) + if err != nil { + t.Fatal("Failed to get raw connection:", err) + } + // Test data expected := []byte("I ❤️ SQLite in \x00\x01\x02…") rowid := int64(6581) From 286623505d3f219e9b17a0c9908a92957d77afbd Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 2 Sep 2022 09:29:30 +0200 Subject: [PATCH 04/20] Blob I/O: doc --- blob_io.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blob_io.go b/blob_io.go index e573273c..ada3972a 100644 --- a/blob_io.go +++ b/blob_io.go @@ -31,7 +31,9 @@ type SQLiteBlob struct { // Blob opens a blob. // -// The flag parameter is ignored. +// See https://www.sqlite.org/c3ref/blob_open.html for usage. +// +// Should only be used with conn.Raw. The flag parameter is ignored. func (conn *SQLiteConn) Blob(database, table, column string, rowid int64, flags int) (*SQLiteBlob, error) { databaseptr := C.CString(database) defer C.free(unsafe.Pointer(databaseptr)) From 867ec21fc3ca279a8f3a0da7aba72d3ea089044e Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 2 Sep 2022 09:35:18 +0200 Subject: [PATCH 05/20] =?UTF-8?q?Blob=20I/O:=20unabbreviate=20offs=20?= =?UTF-8?q?=E2=86=92=20offset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blob_io.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/blob_io.go b/blob_io.go index ada3972a..6891b331 100644 --- a/blob_io.go +++ b/blob_io.go @@ -23,10 +23,10 @@ import ( // SQLiteBlob implements the SQLite Blob I/O interface. type SQLiteBlob struct { - conn *SQLiteConn - blob *C.sqlite3_blob - size int - offs int + conn *SQLiteConn + blob *C.sqlite3_blob + size int + offset int } // Blob opens a blob. @@ -61,22 +61,22 @@ func (conn *SQLiteConn) Blob(database, table, column string, rowid int64, flags // Read implements the io.Reader interface. func (s *SQLiteBlob) Read(b []byte) (n int, err error) { - if s.offs >= s.size { + if s.offset >= s.size { return 0, io.EOF } - n = s.size - s.offs + n = s.size - s.offset if len(b) < n { n = len(b) } p := &b[0] - ret := C.sqlite3_blob_read(s.blob, unsafe.Pointer(p), C.int(n), C.int(s.offs)) + ret := C.sqlite3_blob_read(s.blob, unsafe.Pointer(p), C.int(n), C.int(s.offset)) if ret != C.SQLITE_OK { return 0, s.conn.lastError() } - s.offs += n + s.offset += n return n, nil } From 5583431ad022bb3e2e3b4e8d4bfea3cde81c4c9d Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 2 Sep 2022 09:39:24 +0200 Subject: [PATCH 06/20] Blob I/O: use keys in struct init --- blob_io.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blob_io.go b/blob_io.go index 6891b331..dade1be6 100644 --- a/blob_io.go +++ b/blob_io.go @@ -52,7 +52,7 @@ func (conn *SQLiteConn) Blob(database, table, column string, rowid int64, flags } size := int(C.sqlite3_blob_bytes(blob)) - bb := &SQLiteBlob{conn, blob, size, 0} + bb := &SQLiteBlob{conn: conn, blob: blob, size: size, offset: 0} runtime.SetFinalizer(bb, (*SQLiteBlob).Close) From 606d2d2b3f8c1077f2bb0aab5c4c8b131bec1c23 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 2 Sep 2022 10:00:40 +0200 Subject: [PATCH 07/20] Blob I/O: clean up connection --- blob_io_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/blob_io_test.go b/blob_io_test.go index a33ae1e9..cc36466d 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -37,6 +37,7 @@ func TestBlobIO(t *testing.T) { if err != nil { t.Fatal("Failed to get raw connection:", err) } + defer driverConn.Close() // Test data expected := []byte("I ❤️ SQLite in \x00\x01\x02…") From 79df247797bfe04ed5b0a18537cc265cfece4e8d Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 2 Sep 2022 10:25:44 +0200 Subject: [PATCH 08/20] Blob I/O: move test data / connection --- blob_io_test.go | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/blob_io_test.go b/blob_io_test.go index cc36466d..cd7b0838 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -16,16 +16,15 @@ import ( "testing" ) -func TestBlobIO(t *testing.T) { - db, err := sql.Open("sqlite3", "file:testblobio?mode=memory&cache=shared") +func blobTestData(dbname string, rowid int64, blob []byte) (*sql.DB, *SQLiteConn, error) { + db, err := sql.Open("sqlite3", "file:"+dbname+"?mode=memory&cache=shared") if err != nil { - t.Fatal("Fail to open:", err) + return nil, nil, err } - defer db.Close() conn, err := db.Conn(context.Background()) if err != nil { - t.Fatal("Failed to get raw connection:", err) + return nil, nil, err } defer conn.Close() @@ -35,13 +34,8 @@ func TestBlobIO(t *testing.T) { return nil }) if err != nil { - t.Fatal("Failed to get raw connection:", err) + return nil, nil, err } - defer driverConn.Close() - - // Test data - expected := []byte("I ❤️ SQLite in \x00\x01\x02…") - rowid := int64(6581) query := ` CREATE TABLE data ( @@ -52,10 +46,24 @@ func TestBlobIO(t *testing.T) { VALUES (:rowid, :value); ` - _, err = db.Exec(query, sql.Named("rowid", rowid), sql.Named("value", expected)) + _, err = db.Exec(query, sql.Named("rowid", rowid), sql.Named("value", blob)) + if err != nil { + return nil, nil, err + } + + return db, driverConn, nil +} + +func TestBlobIO(t *testing.T) { + rowid := int64(6581) + expected := []byte("I ❤️ SQLite in \x00\x01\x02…") + + db, driverConn, err := blobTestData("testblobio", rowid, expected) if err != nil { - t.Fatal("Failed to execute", err) + t.Fatal("Failed to get raw connection:", err) } + defer driverConn.Close() + defer db.Close() // Open blob blob, err := driverConn.Blob("main", "data", "value", rowid, 0) From 01dbf4b0a3299c2bcbdd3ae05d38c3985d7d5d28 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Sat, 3 Sep 2022 20:32:08 +0200 Subject: [PATCH 09/20] Blob I/O: add write and seek --- blob_io.go | 53 +++++++++++++++++- blob_io_test.go | 144 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 194 insertions(+), 3 deletions(-) diff --git a/blob_io.go b/blob_io.go index dade1be6..036369c3 100644 --- a/blob_io.go +++ b/blob_io.go @@ -16,7 +16,9 @@ package sqlite3 import "C" import ( + "errors" "io" + "math" "runtime" "unsafe" ) @@ -33,7 +35,7 @@ type SQLiteBlob struct { // // See https://www.sqlite.org/c3ref/blob_open.html for usage. // -// Should only be used with conn.Raw. The flag parameter is ignored. +// Should only be used with conn.Raw. func (conn *SQLiteConn) Blob(database, table, column string, rowid int64, flags int) (*SQLiteBlob, error) { databaseptr := C.CString(database) defer C.free(unsafe.Pointer(databaseptr)) @@ -81,6 +83,55 @@ func (s *SQLiteBlob) Read(b []byte) (n int, err error) { return n, nil } +// Write implements the io.Writer interface. +func (s *SQLiteBlob) Write(b []byte) (n int, err error) { + if s.offset >= s.size { + return 0, io.EOF + } + + n = s.size - s.offset + if len(b) < n { + n = len(b) + } + + p := &b[0] + ret := C.sqlite3_blob_write(s.blob, unsafe.Pointer(p), C.int(n), C.int(s.offset)) + if ret != C.SQLITE_OK { + return 0, s.conn.lastError() + } + + s.offset += n + + return n, nil +} + +// Seek implements the io.Seeker interface. +func (s *SQLiteBlob) Seek(offset int64, whence int) (int64, error) { + if offset > math.MaxInt32 { + return 0, errors.New("sqlite3.SQLiteBlob.Seek: invalid position") + } + + var abs int64 + switch whence { + case io.SeekStart: + abs = offset + case io.SeekCurrent: + abs = int64(s.offset) + offset + case io.SeekEnd: + abs = int64(s.size) + offset + default: + return 0, errors.New("sqlite3.SQLiteBlob.Seek: invalid whence") + } + + if abs < 0 { + return 0, errors.New("sqlite.SQLiteBlob.Seek: negative position") + } + + s.offset = int(abs) + + return abs, nil +} + // Close implements the io.Closer interface. func (s *SQLiteBlob) Close() error { ret := C.sqlite3_blob_close(s.blob) diff --git a/blob_io_test.go b/blob_io_test.go index cd7b0838..5bc051e8 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -16,6 +16,12 @@ import ( "testing" ) +// Verify interface implementations +var _ io.Reader = &SQLiteBlob{} +var _ io.Writer = &SQLiteBlob{} +var _ io.Seeker = &SQLiteBlob{} +var _ io.Closer = &SQLiteBlob{} + func blobTestData(dbname string, rowid int64, blob []byte) (*sql.DB, *SQLiteConn, error) { db, err := sql.Open("sqlite3", "file:"+dbname+"?mode=memory&cache=shared") if err != nil { @@ -54,11 +60,11 @@ func blobTestData(dbname string, rowid int64, blob []byte) (*sql.DB, *SQLiteConn return db, driverConn, nil } -func TestBlobIO(t *testing.T) { +func TestBlobRead(t *testing.T) { rowid := int64(6581) expected := []byte("I ❤️ SQLite in \x00\x01\x02…") - db, driverConn, err := blobTestData("testblobio", rowid, expected) + db, driverConn, err := blobTestData("testblobread", rowid, expected) if err != nil { t.Fatal("Failed to get raw connection:", err) } @@ -109,3 +115,137 @@ func TestBlobIO(t *testing.T) { t.Error("Expected EOF", err) } } + +func TestBlobWrite(t *testing.T) { + rowid := int64(8580) + expected := []byte{ + // Random data from /dev/urandom + 0xe5, 0x48, 0x94, 0xad, 0xa6, 0x7c, 0x81, 0xa2, 0x70, 0x07, 0x79, 0x60, + 0x33, 0xbc, 0x64, 0x33, 0x8f, 0x48, 0x43, 0xa6, 0x33, 0x5c, 0x08, 0x32, + } + + // Allocate a zero blob + data := make([]byte, len(expected)) + db, driverConn, err := blobTestData("testblobwrite", rowid, data) + if err != nil { + t.Fatal("Failed to get raw connection:", err) + } + defer driverConn.Close() + defer db.Close() + + // Open blob for read/write + blob, err := driverConn.Blob("main", "data", "value", rowid, 1) + if err != nil { + t.Error("failed", err) + } + defer blob.Close() + + // Write blob incrementally + middle := len(expected) / 2 + first := expected[:middle] + second := expected[middle:] + + // Write part Ⅰ + n1, err := blob.Write(first) + + if err != nil || n1 != len(first) { + t.Errorf("Failed to write %d bytes", n1) + } + + // Write part Ⅱ + n2, err := blob.Write(second) + + if err != nil || n2 != len(second) { + t.Errorf("Failed to write %d bytes", n2) + } + + // EOF + b3 := make([]byte, 10) + n3, err := blob.Write(b3) + + if err != io.EOF || n3 != 0 { + t.Error("Expected EOF", err) + } + + // Verify written data + _, err = blob.Seek(0, io.SeekStart) + if err != nil { + t.Fatal("Failed to seek:", err) + } + + b4 := make([]byte, len(expected)) + n4, err := blob.Read(b4) + + if err != nil || n4 != len(b4) { + t.Errorf("Failed to read %d bytes", n4) + } + + if bytes.Compare(expected, b4) != 0 { + t.Error("Expected\n", expected, "got\n", b4) + } +} + +func TestBlobSeek(t *testing.T) { + rowid := int64(6510) + data := make([]byte, 1000) + + db, driverConn, err := blobTestData("testblobseek", rowid, data) + if err != nil { + t.Fatal("Failed to get raw connection:", err) + } + defer driverConn.Close() + defer db.Close() + + // Open blob + blob, err := driverConn.Blob("main", "data", "value", rowid, 0) + if err != nil { + t.Error("failed", err) + } + defer blob.Close() + + // Test data + begin := int64(0) + middle := int64(len(data) / 2) + end := int64(len(data) - 1) + eof := int64(len(data)) + + tests := []struct { + offset int64 + whence int + expected int64 + }{ + {offset: begin, whence: io.SeekStart, expected: begin}, + {offset: middle, whence: io.SeekStart, expected: middle}, + {offset: end, whence: io.SeekStart, expected: end}, + {offset: eof, whence: io.SeekStart, expected: eof}, + + {offset: -1, whence: io.SeekCurrent, expected: middle - 1}, + {offset: 0, whence: io.SeekCurrent, expected: middle}, + {offset: 1, whence: io.SeekCurrent, expected: middle + 1}, + {offset: -middle, whence: io.SeekCurrent, expected: begin}, + + {offset: -2, whence: io.SeekEnd, expected: end - 1}, + {offset: -1, whence: io.SeekEnd, expected: end}, + {offset: 0, whence: io.SeekEnd, expected: eof}, + {offset: 1, whence: io.SeekEnd, expected: eof + 1}, + {offset: -eof, whence: io.SeekEnd, expected: begin}, + } + + for _, tc := range tests { + // Start in the middle + _, err := blob.Seek(middle, io.SeekStart) + if err != nil { + t.Fatal("Failed to seek:", err) + } + + // Test + got, err := blob.Seek(tc.offset, tc.whence) + if err != nil { + t.Fatal("Failed to seek:", err) + } + + if tc.expected != got { + t.Error("For", tc, "expected", tc.expected, "got", got) + } + } +} From 893e77dc0a913c7d2c00b927158ad58995465519 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Wed, 7 Sep 2022 09:30:29 +0200 Subject: [PATCH 10/20] Blob I/O: format error --- blob_io.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blob_io.go b/blob_io.go index 036369c3..6070f662 100644 --- a/blob_io.go +++ b/blob_io.go @@ -17,6 +17,7 @@ import "C" import ( "errors" + "fmt" "io" "math" "runtime" @@ -120,7 +121,7 @@ func (s *SQLiteBlob) Seek(offset int64, whence int) (int64, error) { case io.SeekEnd: abs = int64(s.size) + offset default: - return 0, errors.New("sqlite3.SQLiteBlob.Seek: invalid whence") + return 0, fmt.Errorf("sqlite3.SQLiteBlob.Seek: invalid whence %d", whence) } if abs < 0 { From 7df73e0a17079b19b0e713cbe3be7b2d5cf085c9 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 9 Sep 2022 08:39:26 +0200 Subject: [PATCH 11/20] Blob I/O: limit and expose blob size --- blob_io.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/blob_io.go b/blob_io.go index 6070f662..281c7b2d 100644 --- a/blob_io.go +++ b/blob_io.go @@ -128,11 +128,20 @@ func (s *SQLiteBlob) Seek(offset int64, whence int) (int64, error) { return 0, errors.New("sqlite.SQLiteBlob.Seek: negative position") } + if abs > math.MaxInt32 { + return 0, errors.New("sqlite3.SQLiteBlob.Seek: overflow position") + } + s.offset = int(abs) return abs, nil } +// Size returns the size of the blob. +func (s *SQLiteBlob) Size() int { + return s.size +} + // Close implements the io.Closer interface. func (s *SQLiteBlob) Close() error { ret := C.sqlite3_blob_close(s.blob) From e519482e27353c5a74d523f00923616477fbe8ab Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 9 Sep 2022 09:22:29 +0200 Subject: [PATCH 12/20] Blob I/O: correct usage of raw connection --- blob_io_test.go | 288 ++++++++++++++++++++++++------------------------ 1 file changed, 147 insertions(+), 141 deletions(-) diff --git a/blob_io_test.go b/blob_io_test.go index 5bc051e8..7f222f44 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -22,15 +22,17 @@ var _ io.Writer = &SQLiteBlob{} var _ io.Seeker = &SQLiteBlob{} var _ io.Closer = &SQLiteBlob{} -func blobTestData(dbname string, rowid int64, blob []byte) (*sql.DB, *SQLiteConn, error) { +type driverConnCallback func(*testing.T, *SQLiteConn) + +func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c driverConnCallback) error { db, err := sql.Open("sqlite3", "file:"+dbname+"?mode=memory&cache=shared") if err != nil { - return nil, nil, err + return err } conn, err := db.Conn(context.Background()) if err != nil { - return nil, nil, err + return err } defer conn.Close() @@ -40,8 +42,9 @@ func blobTestData(dbname string, rowid int64, blob []byte) (*sql.DB, *SQLiteConn return nil }) if err != nil { - return nil, nil, err + return err } + defer driverConn.Close() query := ` CREATE TABLE data ( @@ -54,65 +57,67 @@ func blobTestData(dbname string, rowid int64, blob []byte) (*sql.DB, *SQLiteConn _, err = db.Exec(query, sql.Named("rowid", rowid), sql.Named("value", blob)) if err != nil { - return nil, nil, err + return err } - return db, driverConn, nil + c(t, driverConn) + + return nil } func TestBlobRead(t *testing.T) { rowid := int64(6581) expected := []byte("I ❤️ SQLite in \x00\x01\x02…") - db, driverConn, err := blobTestData("testblobread", rowid, expected) - if err != nil { - t.Fatal("Failed to get raw connection:", err) - } - defer driverConn.Close() - defer db.Close() + err := blobTestData(t, "testblobread", rowid, expected, func(t *testing.T, driverConn *SQLiteConn) { - // Open blob - blob, err := driverConn.Blob("main", "data", "value", rowid, 0) - if err != nil { - t.Error("failed", err) - } - defer blob.Close() + // Open blob + blob, err := driverConn.Blob("main", "data", "value", rowid, 0) + if err != nil { + t.Error("failed", err) + } + defer blob.Close() - // Read blob incrementally - middle := len(expected) / 2 - first := expected[:middle] - second := expected[middle:] + // Read blob incrementally + middle := len(expected) / 2 + first := expected[:middle] + second := expected[middle:] - // Read part Ⅰ - b1 := make([]byte, len(first)) - n1, err := blob.Read(b1) + // Read part Ⅰ + b1 := make([]byte, len(first)) + n1, err := blob.Read(b1) - if err != nil || n1 != len(b1) { - t.Errorf("Failed to read %d bytes", n1) - } + if err != nil || n1 != len(b1) { + t.Errorf("Failed to read %d bytes", n1) + } - if bytes.Compare(first, b1) != 0 { - t.Error("Expected\n", first, "got\n", b1) - } + if bytes.Compare(first, b1) != 0 { + t.Error("Expected\n", first, "got\n", b1) + } - // Read part Ⅱ - b2 := make([]byte, len(second)) - n2, err := blob.Read(b2) + // Read part Ⅱ + b2 := make([]byte, len(second)) + n2, err := blob.Read(b2) - if err != nil || n2 != len(b2) { - t.Errorf("Failed to read %d bytes", n2) - } + if err != nil || n2 != len(b2) { + t.Errorf("Failed to read %d bytes", n2) + } - if bytes.Compare(second, b2) != 0 { - t.Error("Expected\n", second, "got\n", b2) - } + if bytes.Compare(second, b2) != 0 { + t.Error("Expected\n", second, "got\n", b2) + } + + // EOF + b3 := make([]byte, 10) + n3, err := blob.Read(b3) - // EOF - b3 := make([]byte, 10) - n3, err := blob.Read(b3) + if err != io.EOF || n3 != 0 { + t.Error("Expected EOF", err) + } + }) - if err != io.EOF || n3 != 0 { - t.Error("Expected EOF", err) + if err != nil { + t.Fatal("Failed to get raw connection:", err) } } @@ -126,62 +131,62 @@ func TestBlobWrite(t *testing.T) { // Allocate a zero blob data := make([]byte, len(expected)) - db, driverConn, err := blobTestData("testblobwrite", rowid, data) - if err != nil { - t.Fatal("Failed to get raw connection:", err) - } - defer driverConn.Close() - defer db.Close() + err := blobTestData(t, "testblobwrite", rowid, data, func(t *testing.T, driverConn *SQLiteConn) { - // Open blob for read/write - blob, err := driverConn.Blob("main", "data", "value", rowid, 1) - if err != nil { - t.Error("failed", err) - } - defer blob.Close() + // Open blob for read/write + blob, err := driverConn.Blob("main", "data", "value", rowid, 1) + if err != nil { + t.Error("failed", err) + } + defer blob.Close() - // Write blob incrementally - middle := len(expected) / 2 - first := expected[:middle] - second := expected[middle:] + // Write blob incrementally + middle := len(expected) / 2 + first := expected[:middle] + second := expected[middle:] - // Write part Ⅰ - n1, err := blob.Write(first) + // Write part Ⅰ + n1, err := blob.Write(first) - if err != nil || n1 != len(first) { - t.Errorf("Failed to write %d bytes", n1) - } + if err != nil || n1 != len(first) { + t.Errorf("Failed to write %d bytes", n1) + } - // Write part Ⅱ - n2, err := blob.Write(second) + // Write part Ⅱ + n2, err := blob.Write(second) - if err != nil || n2 != len(second) { - t.Errorf("Failed to write %d bytes", n2) - } + if err != nil || n2 != len(second) { + t.Errorf("Failed to write %d bytes", n2) + } - // EOF - b3 := make([]byte, 10) - n3, err := blob.Write(b3) + // EOF + b3 := make([]byte, 10) + n3, err := blob.Write(b3) - if err != io.EOF || n3 != 0 { - t.Error("Expected EOF", err) - } + if err != io.EOF || n3 != 0 { + t.Error("Expected EOF", err) + } - // Verify written data - _, err = blob.Seek(0, io.SeekStart) - if err != nil { - t.Fatal("Failed to seek:", err) - } + // Verify written data + _, err = blob.Seek(0, io.SeekStart) + if err != nil { + t.Fatal("Failed to seek:", err) + } - b4 := make([]byte, len(expected)) - n4, err := blob.Read(b4) + b4 := make([]byte, len(expected)) + n4, err := blob.Read(b4) - if err != nil || n4 != len(b4) { - t.Errorf("Failed to read %d bytes", n4) - } + if err != nil || n4 != len(b4) { + t.Errorf("Failed to read %d bytes", n4) + } + + if bytes.Compare(expected, b4) != 0 { + t.Error("Expected\n", expected, "got\n", b4) + } - if bytes.Compare(expected, b4) != 0 { - t.Error("Expected\n", expected, "got\n", b4) + }) + if err != nil { + t.Fatal("Failed to get raw connection:", err) } } @@ -189,63 +194,64 @@ func TestBlobSeek(t *testing.T) { rowid := int64(6510) data := make([]byte, 1000) - db, driverConn, err := blobTestData("testblobseek", rowid, data) - if err != nil { - t.Fatal("Failed to get raw connection:", err) - } - defer driverConn.Close() - defer db.Close() - - // Open blob - blob, err := driverConn.Blob("main", "data", "value", rowid, 0) - if err != nil { - t.Error("failed", err) - } - defer blob.Close() - - // Test data - begin := int64(0) - middle := int64(len(data) / 2) - end := int64(len(data) - 1) - eof := int64(len(data)) - - tests := []struct { - offset int64 - whence int - expected int64 - }{ - {offset: begin, whence: io.SeekStart, expected: begin}, - {offset: middle, whence: io.SeekStart, expected: middle}, - {offset: end, whence: io.SeekStart, expected: end}, - {offset: eof, whence: io.SeekStart, expected: eof}, - - {offset: -1, whence: io.SeekCurrent, expected: middle - 1}, - {offset: 0, whence: io.SeekCurrent, expected: middle}, - {offset: 1, whence: io.SeekCurrent, expected: middle + 1}, - {offset: -middle, whence: io.SeekCurrent, expected: begin}, - - {offset: -2, whence: io.SeekEnd, expected: end - 1}, - {offset: -1, whence: io.SeekEnd, expected: end}, - {offset: 0, whence: io.SeekEnd, expected: eof}, - {offset: 1, whence: io.SeekEnd, expected: eof + 1}, - {offset: -eof, whence: io.SeekEnd, expected: begin}, - } + err := blobTestData(t, "testblobseek", rowid, data, func(t *testing.T, driverConn *SQLiteConn) { - for _, tc := range tests { - // Start in the middle - _, err := blob.Seek(middle, io.SeekStart) + // Open blob + blob, err := driverConn.Blob("main", "data", "value", rowid, 0) if err != nil { - t.Fatal("Failed to seek:", err) + t.Error("failed", err) } - - // Test - got, err := blob.Seek(tc.offset, tc.whence) - if err != nil { - t.Fatal("Failed to seek:", err) + defer blob.Close() + + // Test data + begin := int64(0) + middle := int64(len(data) / 2) + end := int64(len(data) - 1) + eof := int64(len(data)) + + tests := []struct { + offset int64 + whence int + expected int64 + }{ + {offset: begin, whence: io.SeekStart, expected: begin}, + {offset: middle, whence: io.SeekStart, expected: middle}, + {offset: end, whence: io.SeekStart, expected: end}, + {offset: eof, whence: io.SeekStart, expected: eof}, + + {offset: -1, whence: io.SeekCurrent, expected: middle - 1}, + {offset: 0, whence: io.SeekCurrent, expected: middle}, + {offset: 1, whence: io.SeekCurrent, expected: middle + 1}, + {offset: -middle, whence: io.SeekCurrent, expected: begin}, + + {offset: -2, whence: io.SeekEnd, expected: end - 1}, + {offset: -1, whence: io.SeekEnd, expected: end}, + {offset: 0, whence: io.SeekEnd, expected: eof}, + {offset: 1, whence: io.SeekEnd, expected: eof + 1}, + {offset: -eof, whence: io.SeekEnd, expected: begin}, } - if tc.expected != got { - t.Error("For", tc, "expected", tc.expected, "got", got) + for _, tc := range tests { + // Start in the middle + _, err := blob.Seek(middle, io.SeekStart) + if err != nil { + t.Fatal("Failed to seek:", err) + } + + // Test + got, err := blob.Seek(tc.offset, tc.whence) + if err != nil { + t.Fatal("Failed to seek:", err) + } + + if tc.expected != got { + t.Error("For", tc, "expected", tc.expected, "got", got) + } } + + }) + + if err != nil { + t.Fatal("Failed to get raw connection:", err) } } From 67dc006d8591202d1b5024b49376a3234a779224 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Fri, 9 Sep 2022 09:40:03 +0200 Subject: [PATCH 13/20] Blob I/O: close db --- blob_io_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/blob_io_test.go b/blob_io_test.go index 7f222f44..52e37f4a 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -29,6 +29,7 @@ func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c drive if err != nil { return err } + defer db.Close() conn, err := db.Conn(context.Background()) if err != nil { From 7f0a6fb85d4bd943d7dba7d29a86e004684c1f53 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Sun, 11 Sep 2022 13:13:09 +0200 Subject: [PATCH 14/20] Blob I/O: use vfsdb --- blob_io_test.go | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/blob_io_test.go b/blob_io_test.go index 52e37f4a..dbcf143e 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -25,12 +25,28 @@ var _ io.Closer = &SQLiteBlob{} type driverConnCallback func(*testing.T, *SQLiteConn) func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c driverConnCallback) error { - db, err := sql.Open("sqlite3", "file:"+dbname+"?mode=memory&cache=shared") + db, err := sql.Open("sqlite3", "file:/"+dbname+"?vfs=memdb") if err != nil { return err } defer db.Close() + // Test data + query := ` + CREATE TABLE data ( + value BLOB + ); + + INSERT INTO data (_rowid_, value) + VALUES (:rowid, :value); + ` + + _, err = db.Exec(query, sql.Named("rowid", rowid), sql.Named("value", blob)) + if err != nil { + return err + } + + // Get raw connection conn, err := db.Conn(context.Background()) if err != nil { return err @@ -47,20 +63,6 @@ func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c drive } defer driverConn.Close() - query := ` - CREATE TABLE data ( - value BLOB - ); - - INSERT INTO data (_rowid_, value) - VALUES (:rowid, :value); - ` - - _, err = db.Exec(query, sql.Named("rowid", rowid), sql.Named("value", blob)) - if err != nil { - return err - } - c(t, driverConn) return nil From 5b53687276e792c7246e26e3a729424debf82c83 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Sun, 25 Sep 2022 19:55:49 +0200 Subject: [PATCH 15/20] Blob I/O: insufficient space errors for Write, raw connection callback --- blob_io.go | 6 +++++- blob_io_test.go | 9 ++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/blob_io.go b/blob_io.go index 281c7b2d..c0369f37 100644 --- a/blob_io.go +++ b/blob_io.go @@ -87,7 +87,7 @@ func (s *SQLiteBlob) Read(b []byte) (n int, err error) { // Write implements the io.Writer interface. func (s *SQLiteBlob) Write(b []byte) (n int, err error) { if s.offset >= s.size { - return 0, io.EOF + return 0, fmt.Errorf("sqlite3.SQLiteBlob.Write: insufficient space in %d-byte blob", s.size) } n = s.size - s.offset @@ -95,6 +95,10 @@ func (s *SQLiteBlob) Write(b []byte) (n int, err error) { n = len(b) } + if n != len(b) { + return n, fmt.Errorf("sqlite3.SQLiteBlob.Write: insufficient space in %d-byte blob", s.size) + } + p := &b[0] ret := C.sqlite3_blob_write(s.blob, unsafe.Pointer(p), C.int(n), C.int(s.offset)) if ret != C.SQLITE_OK { diff --git a/blob_io_test.go b/blob_io_test.go index dbcf143e..cedd8bc0 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -56,6 +56,7 @@ func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c drive var driverConn *SQLiteConn err = conn.Raw(func(conn interface{}) error { driverConn = conn.(*SQLiteConn) + c(t, driverConn) return nil }) if err != nil { @@ -63,8 +64,6 @@ func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c drive } defer driverConn.Close() - c(t, driverConn) - return nil } @@ -162,12 +161,12 @@ func TestBlobWrite(t *testing.T) { t.Errorf("Failed to write %d bytes", n2) } - // EOF + // Insufficient space b3 := make([]byte, 10) n3, err := blob.Write(b3) - if err != io.EOF || n3 != 0 { - t.Error("Expected EOF", err) + if err.Error() != "sqlite3.SQLiteBlob.Write: insufficient space in 24-byte blob" || n3 != 0 { + t.Error("Expected insufficient space error", err, n3) } // Verify written data From ef6fa5a0cc2fedd9a90d0ca3d25518b7795bb23a Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Wed, 19 Oct 2022 09:01:37 +0200 Subject: [PATCH 16/20] Blob I/O: helpful error Co-authored-by: rittneje --- blob_io.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blob_io.go b/blob_io.go index c0369f37..488e1502 100644 --- a/blob_io.go +++ b/blob_io.go @@ -113,7 +113,7 @@ func (s *SQLiteBlob) Write(b []byte) (n int, err error) { // Seek implements the io.Seeker interface. func (s *SQLiteBlob) Seek(offset int64, whence int) (int64, error) { if offset > math.MaxInt32 { - return 0, errors.New("sqlite3.SQLiteBlob.Seek: invalid position") + return 0, fmt.Errorf("sqlite3.SQLiteBlob.Seek: invalid offset %d", offset) } var abs int64 From fe0ed2efdb4f64756de3db6954c521d624ac9786 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Wed, 26 Oct 2022 10:42:54 +0200 Subject: [PATCH 17/20] Blob I/O: adhere to io.Writer --- blob_io.go | 4 ++-- blob_io_test.go | 30 ++++++++---------------------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/blob_io.go b/blob_io.go index 488e1502..a34b060d 100644 --- a/blob_io.go +++ b/blob_io.go @@ -96,7 +96,7 @@ func (s *SQLiteBlob) Write(b []byte) (n int, err error) { } if n != len(b) { - return n, fmt.Errorf("sqlite3.SQLiteBlob.Write: insufficient space in %d-byte blob", s.size) + return 0, fmt.Errorf("sqlite3.SQLiteBlob.Write: insufficient space in %d-byte blob", s.size) } p := &b[0] @@ -132,7 +132,7 @@ func (s *SQLiteBlob) Seek(offset int64, whence int) (int64, error) { return 0, errors.New("sqlite.SQLiteBlob.Seek: negative position") } - if abs > math.MaxInt32 { + if abs > math.MaxInt32 || abs > int64(s.size) { return 0, errors.New("sqlite3.SQLiteBlob.Seek: overflow position") } diff --git a/blob_io_test.go b/blob_io_test.go index cedd8bc0..3e6fb915 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -24,10 +24,10 @@ var _ io.Closer = &SQLiteBlob{} type driverConnCallback func(*testing.T, *SQLiteConn) -func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c driverConnCallback) error { +func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c driverConnCallback) { db, err := sql.Open("sqlite3", "file:/"+dbname+"?vfs=memdb") if err != nil { - return err + t.Fatal(err) } defer db.Close() @@ -43,13 +43,13 @@ func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c drive _, err = db.Exec(query, sql.Named("rowid", rowid), sql.Named("value", blob)) if err != nil { - return err + t.Fatal(err) } // Get raw connection conn, err := db.Conn(context.Background()) if err != nil { - return err + t.Fatal(err) } defer conn.Close() @@ -60,18 +60,16 @@ func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c drive return nil }) if err != nil { - return err + t.Fatal(err) } defer driverConn.Close() - - return nil } func TestBlobRead(t *testing.T) { rowid := int64(6581) expected := []byte("I ❤️ SQLite in \x00\x01\x02…") - err := blobTestData(t, "testblobread", rowid, expected, func(t *testing.T, driverConn *SQLiteConn) { + blobTestData(t, "testblobread", rowid, expected, func(t *testing.T, driverConn *SQLiteConn) { // Open blob blob, err := driverConn.Blob("main", "data", "value", rowid, 0) @@ -117,10 +115,6 @@ func TestBlobRead(t *testing.T) { t.Error("Expected EOF", err) } }) - - if err != nil { - t.Fatal("Failed to get raw connection:", err) - } } func TestBlobWrite(t *testing.T) { @@ -133,7 +127,7 @@ func TestBlobWrite(t *testing.T) { // Allocate a zero blob data := make([]byte, len(expected)) - err := blobTestData(t, "testblobwrite", rowid, data, func(t *testing.T, driverConn *SQLiteConn) { + blobTestData(t, "testblobwrite", rowid, data, func(t *testing.T, driverConn *SQLiteConn) { // Open blob for read/write blob, err := driverConn.Blob("main", "data", "value", rowid, 1) @@ -187,16 +181,13 @@ func TestBlobWrite(t *testing.T) { } }) - if err != nil { - t.Fatal("Failed to get raw connection:", err) - } } func TestBlobSeek(t *testing.T) { rowid := int64(6510) data := make([]byte, 1000) - err := blobTestData(t, "testblobseek", rowid, data, func(t *testing.T, driverConn *SQLiteConn) { + blobTestData(t, "testblobseek", rowid, data, func(t *testing.T, driverConn *SQLiteConn) { // Open blob blob, err := driverConn.Blob("main", "data", "value", rowid, 0) @@ -229,7 +220,6 @@ func TestBlobSeek(t *testing.T) { {offset: -2, whence: io.SeekEnd, expected: end - 1}, {offset: -1, whence: io.SeekEnd, expected: end}, {offset: 0, whence: io.SeekEnd, expected: eof}, - {offset: 1, whence: io.SeekEnd, expected: eof + 1}, {offset: -eof, whence: io.SeekEnd, expected: begin}, } @@ -252,8 +242,4 @@ func TestBlobSeek(t *testing.T) { } }) - - if err != nil { - t.Fatal("Failed to get raw connection:", err) - } } From cb853d02a9aa3e8d7a4cc70fa3b2717a40e5a779 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Wed, 30 Nov 2022 10:36:27 +0100 Subject: [PATCH 18/20] Blob I/O: use :memory: for compatibility (SQLite <3.37.0) --- blob_io_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/blob_io_test.go b/blob_io_test.go index 3e6fb915..a699951c 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -25,12 +25,19 @@ var _ io.Closer = &SQLiteBlob{} type driverConnCallback func(*testing.T, *SQLiteConn) func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c driverConnCallback) { - db, err := sql.Open("sqlite3", "file:/"+dbname+"?vfs=memdb") + + // TODO use :memory: for compatibility with SQLite versions < 3.37.0. + // Use memdb vfs for more recent versions. + + // db, err := sql.Open("sqlite3", "file:/"+dbname+"?vfs=memdb") + db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatal(err) } defer db.Close() + db.SetMaxOpenConns(1) + // Test data query := ` CREATE TABLE data ( From 7d1086007d197e0e828b5b5e888879c04b544303 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Sun, 8 Oct 2023 20:24:54 +0200 Subject: [PATCH 19/20] Blob I/O: add extra checks after review --- blob_io.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/blob_io.go b/blob_io.go index a34b060d..2acf2133 100644 --- a/blob_io.go +++ b/blob_io.go @@ -68,6 +68,10 @@ func (s *SQLiteBlob) Read(b []byte) (n int, err error) { return 0, io.EOF } + if len(b) == 0 { + return 0, nil + } + n = s.size - s.offset if len(b) < n { n = len(b) @@ -86,6 +90,10 @@ func (s *SQLiteBlob) Read(b []byte) (n int, err error) { // Write implements the io.Writer interface. func (s *SQLiteBlob) Write(b []byte) (n int, err error) { + if len(b) == 0 { + return 0, nil + } + if s.offset >= s.size { return 0, fmt.Errorf("sqlite3.SQLiteBlob.Write: insufficient space in %d-byte blob", s.size) } From 3f8fe99cb3055750048137d0999037be8c2f1919 Mon Sep 17 00:00:00 2001 From: Joris Zwart Date: Sun, 22 Oct 2023 12:16:28 +0200 Subject: [PATCH 20/20] Blob I/O: fix TODO --- blob_io_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blob_io_test.go b/blob_io_test.go index a699951c..01941ca1 100644 --- a/blob_io_test.go +++ b/blob_io_test.go @@ -26,8 +26,8 @@ type driverConnCallback func(*testing.T, *SQLiteConn) func blobTestData(t *testing.T, dbname string, rowid int64, blob []byte, c driverConnCallback) { - // TODO use :memory: for compatibility with SQLite versions < 3.37.0. - // Use memdb vfs for more recent versions. + // This test uses :memory: for compatibility with SQLite versions < 3.37.0. + // Using memdb vfs is the right way to do this for more recent versions. // db, err := sql.Open("sqlite3", "file:/"+dbname+"?vfs=memdb") db, err := sql.Open("sqlite3", ":memory:")