From e8890879d38cf1683a506bf4cf426eb71f79f1a9 Mon Sep 17 00:00:00 2001 From: Andy Walker Date: Mon, 25 Mar 2024 23:11:34 -0400 Subject: [PATCH 1/4] feat: ResultBytes - Add ResultBytes to sqlitex to support one-result BLOB queries - Export ErrNoResults/ErrMultipleResults so the user can react to these conditions programmatically. --- sqlitex/query.go | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/sqlitex/query.go b/sqlitex/query.go index 7c930e8..63bec91 100644 --- a/sqlitex/query.go +++ b/sqlitex/query.go @@ -9,8 +9,10 @@ import ( "zombiezen.com/go/sqlite" ) -var errNoResults = errors.New("sqlite: statement has no results") -var errMultipleResults = errors.New("sqlite: statement has multiple result rows") +var ( + ErrNoResults = errors.New("sqlite: statement has no results") + ErrMultipleResults = errors.New("sqlite: statement has multiple result rows") +) func resultSetup(stmt *sqlite.Stmt) error { hasRow, err := stmt.Step() @@ -20,7 +22,7 @@ func resultSetup(stmt *sqlite.Stmt) error { } if !hasRow { stmt.Reset() - return errNoResults + return ErrNoResults } return nil } @@ -33,7 +35,7 @@ func resultTeardown(stmt *sqlite.Stmt) error { } if hasRow { stmt.Reset() - return errMultipleResults + return ErrMultipleResults } return stmt.Reset() } @@ -100,3 +102,16 @@ func ResultFloat(stmt *sqlite.Stmt) (float64, error) { } return res, nil } + +// ResultBytes reads the first column of the first and only row produced by running stmt into buf, returning the number of bytes read. +// It returns an error if there is not exactly one result row. +func ResultBytes(stmt *sqlite.Stmt, buf []byte) (int, error) { + if err := resultSetup(stmt); err != nil { + return 0, err + } + read := stmt.ColumnBytes(0, buf) + if err := resultTeardown(stmt); err != nil { + return 0, err + } + return read, nil +} From 67322b694dc84fec100d2da07bce7eaec6bf3215 Mon Sep 17 00:00:00 2001 From: Roxy Light Date: Fri, 16 Aug 2024 08:22:43 -0700 Subject: [PATCH 2/4] Revert error export change --- sqlitex/query.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sqlitex/query.go b/sqlitex/query.go index 63bec91..199e0dc 100644 --- a/sqlitex/query.go +++ b/sqlitex/query.go @@ -10,8 +10,8 @@ import ( ) var ( - ErrNoResults = errors.New("sqlite: statement has no results") - ErrMultipleResults = errors.New("sqlite: statement has multiple result rows") + errNoResults = errors.New("sqlite: statement has no results") + errMultipleResults = errors.New("sqlite: statement has multiple result rows") ) func resultSetup(stmt *sqlite.Stmt) error { @@ -22,7 +22,7 @@ func resultSetup(stmt *sqlite.Stmt) error { } if !hasRow { stmt.Reset() - return ErrNoResults + return errNoResults } return nil } @@ -35,7 +35,7 @@ func resultTeardown(stmt *sqlite.Stmt) error { } if hasRow { stmt.Reset() - return ErrMultipleResults + return errMultipleResults } return stmt.Reset() } @@ -103,7 +103,9 @@ func ResultFloat(stmt *sqlite.Stmt) (float64, error) { return res, nil } -// ResultBytes reads the first column of the first and only row produced by running stmt into buf, returning the number of bytes read. +// ResultBytes reads the first column of the first and only row +// produced by running stmt into buf, +// returning the number of bytes read. // It returns an error if there is not exactly one result row. func ResultBytes(stmt *sqlite.Stmt, buf []byte) (int, error) { if err := resultSetup(stmt); err != nil { From 17c83c3e146d65548a36e36ac49c32fb0b25761b Mon Sep 17 00:00:00 2001 From: Roxy Light Date: Fri, 16 Aug 2024 08:24:57 -0700 Subject: [PATCH 3/4] Add CHANGELOG notes --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b93a0ab..67b8f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased][] +### Added + +- New function `sqlitex.ResultBytes`. + ([#86](https://github.com/zombiezen/go-sqlite/pull/86)) + ### Changed - `Conn.Close` returns an error if the connection has already been closed From 636063785dcd7c8ece5cf2837977ee8228559938 Mon Sep 17 00:00:00 2001 From: Roxy Light Date: Fri, 16 Aug 2024 08:46:51 -0700 Subject: [PATCH 4/4] Add unit tests for Result* functions --- sqlitex/query_test.go | 360 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 sqlitex/query_test.go diff --git a/sqlitex/query_test.go b/sqlitex/query_test.go new file mode 100644 index 0000000..bca63fe --- /dev/null +++ b/sqlitex/query_test.go @@ -0,0 +1,360 @@ +// Copyright 2024 Roxy Light +// SPDX-License-Identifier: ISC + +package sqlitex + +import ( + "testing" + + "zombiezen.com/go/sqlite" +) + +func TestResultInt64(t *testing.T) { + conn, err := sqlite.OpenConn(":memory:") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := conn.Close(); err != nil { + t.Error(err) + } + }() + + err = ExecuteScript(conn, ` +CREATE TABLE foo ( + id integer not null primary key +); + +INSERT INTO foo VALUES (1), (2);`, nil) + if err != nil { + t.Fatal(err) + } + + t.Run("Single", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT 42;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + const want = 42 + got, err := ResultInt64(stmt) + if got != want || err != nil { + t.Errorf("ResultInt64(...) = %d, %v; want %d, ", got, err, want) + } + }) + + t.Run("Multiple", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + n, err := ResultInt64(stmt) + if n != 0 || err == nil { + t.Errorf("ResultInt64(...) = %d, %v; want 0, ", n, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) + + t.Run("NoRows", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo WHERE id > 3;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + n, err := ResultInt64(stmt) + if n != 0 || err == nil { + t.Errorf("ResultInt64(...) = %d, %v; want 0, ", n, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) +} + +func TestResultBool(t *testing.T) { + conn, err := sqlite.OpenConn(":memory:") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := conn.Close(); err != nil { + t.Error(err) + } + }() + + err = ExecuteScript(conn, ` +CREATE TABLE foo ( + id integer not null primary key +); + +INSERT INTO foo VALUES (1), (2);`, nil) + if err != nil { + t.Fatal(err) + } + + t.Run("False", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT false;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + got, err := ResultBool(stmt) + if got || err != nil { + t.Errorf("ResultBool(...) = %t, %v; want false, ", got, err) + } + }) + + t.Run("True", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT true;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + got, err := ResultBool(stmt) + if !got || err != nil { + t.Errorf("ResultBool(...) = %t, %v; want true, ", got, err) + } + }) + + t.Run("Multiple", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT id = 1 FROM foo;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + got, err := ResultBool(stmt) + if got || err == nil { + t.Errorf("ResultBool(...) = %t, %v; want false, ", got, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) + + t.Run("NoRows", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT id = 1 FROM foo WHERE id > 3;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + got, err := ResultBool(stmt) + if got || err == nil { + t.Errorf("ResultBool(...) = %t, %v; want false, ", got, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) +} + +func TestResultText(t *testing.T) { + conn, err := sqlite.OpenConn(":memory:") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := conn.Close(); err != nil { + t.Error(err) + } + }() + + err = ExecuteScript(conn, ` +CREATE TABLE foo ( + id integer not null primary key, + my_blob blob +); + +INSERT INTO foo VALUES (1, CAST('hi' AS BLOB)), (2, CAST('bye' AS BLOB));`, nil) + if err != nil { + t.Fatal(err) + } + + t.Run("Single", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 1;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + const want = "hi" + got, err := ResultText(stmt) + if got != want || err != nil { + t.Errorf("ResultText(...) = %q, %v; want %q, ", got, err, want) + } + }) + + t.Run("Multiple", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + got, err := ResultText(stmt) + if got != "" || err == nil { + t.Errorf("ResultText(...) = %q, %v; want 0, ", got, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) + + t.Run("NoRows", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 3;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + got, err := ResultText(stmt) + if got != "" || err == nil { + t.Errorf("ResultText(...) = %q, %v; want 0, ", got, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) +} + +func TestResultFloat(t *testing.T) { + conn, err := sqlite.OpenConn(":memory:") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := conn.Close(); err != nil { + t.Error(err) + } + }() + + err = ExecuteScript(conn, ` +CREATE TABLE foo ( + id integer not null primary key +); + +INSERT INTO foo VALUES (1), (2);`, nil) + if err != nil { + t.Fatal(err) + } + + t.Run("Single", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT 42;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + const want = 42.0 + got, err := ResultFloat(stmt) + if got != want || err != nil { + t.Errorf("ResultFloat(...) = %g, %v; want %g, ", got, err, want) + } + }) + + t.Run("Multiple", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + n, err := ResultFloat(stmt) + if n != 0 || err == nil { + t.Errorf("ResultFloat(...) = %g, %v; want 0, ", n, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) + + t.Run("NoRows", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT id FROM foo WHERE id > 3;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + n, err := ResultFloat(stmt) + if n != 0 || err == nil { + t.Errorf("ResultFloat(...) = %g, %v; want 0, ", n, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) +} + +func TestResultBytes(t *testing.T) { + conn, err := sqlite.OpenConn(":memory:") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := conn.Close(); err != nil { + t.Error(err) + } + }() + + err = ExecuteScript(conn, ` +CREATE TABLE foo ( + id integer not null primary key, + my_blob blob +); + +INSERT INTO foo VALUES (1, CAST('hi' AS BLOB)), (2, CAST('bye' AS BLOB));`, nil) + if err != nil { + t.Fatal(err) + } + + t.Run("Single", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 1;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + const want = "hi" + buf := make([]byte, 4096) + n, err := ResultBytes(stmt, buf) + if n != len(want) || err != nil { + t.Errorf("ResultBytes(...) = %d, %v; want %d, ", n, err, len(want)) + } + if got := string(buf[:n]); got != want { + t.Errorf("result = %q; want %q", got, want) + } + }) + + t.Run("Multiple", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + buf := make([]byte, 4096) + n, err := ResultBytes(stmt, buf) + if n != 0 || err == nil { + t.Errorf("ResultBytes(...) = %d, %v; want 0, ", n, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) + + t.Run("NoRows", func(t *testing.T) { + stmt, _, err := conn.PrepareTransient(`SELECT my_blob FROM foo WHERE id = 3;`) + if err != nil { + t.Fatal(err) + } + defer stmt.Finalize() + + buf := make([]byte, 4096) + n, err := ResultBytes(stmt, buf) + if n != 0 || err == nil { + t.Errorf("ResultBytes(...) = %d, %v; want 0, ", n, err) + } else { + t.Log("Returned (expected) error:", err) + } + }) +}