Skip to content

Commit b61b897

Browse files
committed
Committing to (what is now called) the Handle interface.
No more package-level/static functions.
1 parent 68e4cc4 commit b61b897

File tree

8 files changed

+99
-296
lines changed

8 files changed

+99
-296
lines changed

config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package sqldb
22

33
type configuration struct {
4-
logger Logger
4+
logger logger
55
threshold int
66
}
77
type option func(*configuration)
@@ -24,7 +24,7 @@ func (singleton) defaults(options ...option) []option {
2424
}, options...)
2525
}
2626

27-
func (singleton) Logger(logger Logger) option {
27+
func (singleton) Logger(logger logger) option {
2828
return func(this *configuration) { this.logger = logger }
2929
}
3030

contracts.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,25 @@ import (
99
var ErrParameterCountMismatch = errors.New("the number of parameters supplied does not match the statement")
1010

1111
type (
12-
Logger interface {
12+
logger interface {
1313
Printf(string, ...any)
1414
}
1515

16-
// Handle is a common subset of methods implemented by *sql.DB and *sql.Tx
17-
Handle interface {
16+
// Pool is a common subset of methods implemented by *sql.DB and *sql.Tx.
17+
// The name is a nod to that fact that a *sql.DB implements a Pool of connections.
18+
Pool interface {
1819
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
1920
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
2021
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
2122
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
2223
}
2324

24-
// DB is a high level approach to common database operations, where each operation implements either
25+
// Handle is a high level approach to common database operations, where each operation implements either
2526
// the Query or Script interface.
26-
DB interface {
27+
Handle interface {
2728
Execute(ctx context.Context, script Script) error
28-
QueryRow(ctx context.Context, query Query) error
29-
Query(ctx context.Context, query Query) error
29+
Populate(ctx context.Context, query Query) error
30+
PopulateRow(ctx context.Context, query Query) error
3031
}
3132

3233
// Script represents SQL statements that aren't expected to provide rows as a result.

db.go

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,32 @@ import (
55
"database/sql"
66
"errors"
77
"fmt"
8+
"iter"
9+
"runtime/debug"
810
"strings"
911
)
1012

11-
type db struct {
12-
handle Handle
13-
logger Logger
13+
type defaultHandle struct {
14+
pool Pool
15+
logger logger
1416
threshold int
1517
counts map[string]int
1618
prepared map[string]*sql.Stmt
1719
}
1820

19-
func New(handle Handle, options ...option) DB {
21+
func New(handle Pool, options ...option) Handle {
2022
var config configuration
2123
Options.apply(options...)(&config)
22-
return &db{
23-
handle: handle,
24+
return &defaultHandle{
25+
pool: handle,
2426
logger: config.logger,
2527
threshold: config.threshold,
2628
counts: make(map[string]int),
2729
prepared: make(map[string]*sql.Stmt),
2830
}
2931
}
3032

31-
func (this *db) prepare(ctx context.Context, rawStatement string) (*sql.Stmt, error) {
33+
func (this *defaultHandle) prepare(ctx context.Context, rawStatement string) (*sql.Stmt, error) {
3234
if this.threshold < 0 {
3335
return nil, nil
3436
}
@@ -40,16 +42,16 @@ func (this *db) prepare(ctx context.Context, rawStatement string) (*sql.Stmt, er
4042
if ok {
4143
return statement, nil
4244
}
43-
statement, err := this.handle.PrepareContext(ctx, rawStatement)
45+
statement, err := this.pool.PrepareContext(ctx, rawStatement)
4446
if err != nil {
4547
return nil, err
4648
}
4749
this.prepared[rawStatement] = statement
4850
return statement, nil
4951
}
5052

51-
func (this *db) Execute(ctx context.Context, script Script) (err error) {
52-
defer func() { err = NormalizeErr(err) }()
53+
func (this *defaultHandle) Execute(ctx context.Context, script Script) (err error) {
54+
defer func() { err = normalizeErr(err) }()
5355
statements := script.Statements()
5456
parameters := script.Parameters()
5557
placeholderCount := strings.Count(statements, "?")
@@ -64,57 +66,89 @@ func (this *db) Execute(ctx context.Context, script Script) (err error) {
6466
if prepared != nil {
6567
_, err = prepared.ExecContext(ctx, params...)
6668
} else {
67-
_, err = this.handle.ExecContext(ctx, statement, params...)
69+
_, err = this.pool.ExecContext(ctx, statement, params...)
6870
}
6971
if err != nil {
7072
return err
7173
}
7274
}
7375
return nil
7476
}
75-
func (this *db) QueryRow(ctx context.Context, query Query) (err error) {
76-
defer func() { err = NormalizeErr(err) }()
77+
func (this *defaultHandle) Populate(ctx context.Context, query Query) (err error) {
78+
defer func() { err = normalizeErr(err) }()
7779
statement := query.Statement()
7880
prepared, err := this.prepare(ctx, statement)
7981
if err != nil {
8082
return err
8183
}
8284
parameters := query.Parameters()
83-
var row *sql.Row
85+
var rows *sql.Rows
8486
if prepared != nil {
85-
row = prepared.QueryRowContext(ctx, parameters...)
87+
rows, err = prepared.QueryContext(ctx, parameters...)
8688
} else {
87-
row = this.handle.QueryRowContext(ctx, statement, parameters...)
89+
rows, err = this.pool.QueryContext(ctx, statement, parameters...)
8890
}
89-
err = query.Scan(row)
90-
if errors.Is(err, sql.ErrNoRows) {
91-
return nil
91+
if err != nil {
92+
return err
9293
}
93-
return err
94+
defer func() { _ = rows.Close() }()
95+
for rows.Next() {
96+
err = query.Scan(rows)
97+
if err != nil {
98+
return err
99+
}
100+
}
101+
return rows.Err()
94102
}
95-
func (this *db) Query(ctx context.Context, query Query) (err error) {
96-
defer func() { err = NormalizeErr(err) }()
103+
func (this *defaultHandle) PopulateRow(ctx context.Context, query Query) (err error) {
104+
defer func() { err = normalizeErr(err) }()
97105
statement := query.Statement()
98106
prepared, err := this.prepare(ctx, statement)
99107
if err != nil {
100108
return err
101109
}
102110
parameters := query.Parameters()
103-
var rows *sql.Rows
111+
var row *sql.Row
104112
if prepared != nil {
105-
rows, err = prepared.QueryContext(ctx, parameters...)
113+
row = prepared.QueryRowContext(ctx, parameters...)
106114
} else {
107-
rows, err = this.handle.QueryContext(ctx, statement, parameters...)
115+
row = this.pool.QueryRowContext(ctx, statement, parameters...)
108116
}
109-
if err != nil {
110-
return err
117+
err = query.Scan(row)
118+
if errors.Is(err, sql.ErrNoRows) {
119+
return nil
111120
}
112-
defer func() { _ = rows.Close() }()
113-
for rows.Next() {
114-
err = query.Scan(rows)
115-
if err != nil {
116-
return err
121+
return err
122+
}
123+
124+
// interleaveParameters splits the statements (on ';') and pairs each one with its corresponding parameters.
125+
func interleaveParameters(statements string, parameters ...any) iter.Seq2[string, []any] {
126+
return func(yield func(string, []any) bool) {
127+
index := 0
128+
for statement := range strings.SplitSeq(statements, ";") {
129+
if len(strings.TrimSpace(statement)) == 0 {
130+
continue
131+
}
132+
statement += ";" // terminate the statement
133+
indexOffset := strings.Count(statement, "?")
134+
params := parameters[index : index+indexOffset]
135+
index += indexOffset
136+
if !yield(statement, params) {
137+
return
138+
}
117139
}
118140
}
119-
return rows.Err()
141+
}
142+
143+
// normalizeErr attaches a stack trace to non-nil errors and also normalizes errors that are
144+
// semantically equal to context.Canceled. At present we are unaware whether this is still a
145+
// commonly encountered scenario.
146+
func normalizeErr(err error) error {
147+
if err == nil {
148+
return nil
149+
}
150+
if strings.Contains(err.Error(), "operation was canceled") {
151+
return fmt.Errorf("%w: %w", context.Canceled, err)
152+
}
153+
return fmt.Errorf("%w\nStack Trace:\n%s", err, string(debug.Stack()))
120154
}

integration/db_test.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"github.com/smarty/gunit/v2/better"
1010
"github.com/smarty/gunit/v2/should"
1111
"github.com/smarty/sqldb/v3"
12+
13+
_ "github.com/mattn/go-sqlite3"
1214
)
1315

1416
func TestFixture(t *testing.T) {
@@ -19,7 +21,7 @@ type Fixture struct {
1921
*gunit.Fixture
2022
db *sql.DB
2123
tx *sql.Tx
22-
DB sqldb.DB
24+
DB sqldb.Handle
2325
}
2426

2527
func (this *Fixture) Setup() {
@@ -45,7 +47,7 @@ func (this *Fixture) Teardown() {
4547
func (this *Fixture) TestQuery() {
4648
for range 10 { // should transition to prepared statements
4749
query := &SelectAll{Result: make(map[int]string)}
48-
err := this.DB.Query(this.Context(), query)
50+
err := this.DB.Populate(this.Context(), query)
4951
this.So(err, better.BeNil)
5052
this.So(query.Result, should.Equal, map[int]string{
5153
1: "a",
@@ -57,13 +59,13 @@ func (this *Fixture) TestQuery() {
5759
}
5860
func (this *Fixture) TestQueryRow() {
5961
query := &SelectRow{id: 1}
60-
err := this.DB.QueryRow(this.Context(), query)
62+
err := this.DB.PopulateRow(this.Context(), query)
6163
this.So(err, better.BeNil)
6264
this.So(query.value, should.Equal, "a")
6365
}
6466
func (this *Fixture) TestQueryQueryRow_NoResult() {
6567
query := &SelectRow{id: 5}
66-
err := this.DB.QueryRow(this.Context(), query)
68+
err := this.DB.PopulateRow(this.Context(), query)
6769
this.So(err, better.BeNil)
6870
this.So(query.value, should.BeEmpty)
6971
}
@@ -73,7 +75,18 @@ func (this *Fixture) TestQueryQueryRow_NoResult() {
7375
type DDL struct{}
7476

7577
func (this *DDL) Statements() string {
76-
return CreateInsert
78+
return `
79+
DROP TABLE IF EXISTS sqldb_integration_test;
80+
81+
CREATE TABLE sqldb_integration_test (
82+
id INTEGER PRIMARY KEY AUTOINCREMENT,
83+
name TEXT NOT NULL
84+
);
85+
86+
INSERT INTO sqldb_integration_test (name) VALUES (?);
87+
INSERT INTO sqldb_integration_test (name) VALUES (?);
88+
INSERT INTO sqldb_integration_test (name) VALUES (?);
89+
INSERT INTO sqldb_integration_test (name) VALUES (?);`
7790
}
7891

7992
func (this *DDL) Parameters() []any {
@@ -92,7 +105,9 @@ type SelectAll struct {
92105
}
93106

94107
func (this *SelectAll) Statement() string {
95-
return QuerySelectAll
108+
return `
109+
SELECT id, name
110+
FROM sqldb_integration_test;`
96111
}
97112

98113
func (this *SelectAll) Parameters() []any {
@@ -114,7 +129,7 @@ type SelectRow struct {
114129
}
115130

116131
func (this *SelectRow) Statement() string {
117-
return "SELECT name FROM sqldb_integration_test WHERE id = ?;"
132+
return `SELECT name FROM sqldb_integration_test WHERE id = ?;`
118133
}
119134
func (this *SelectRow) Parameters() []any {
120135
return []any{this.id}

integration/package_test.go

Lines changed: 0 additions & 76 deletions
This file was deleted.

0 commit comments

Comments
 (0)