aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-07-31 00:23:28 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-07-31 00:23:28 +0200
commit8871cb8f2b8163d803f8f5ad2c2dc2a3f345bb0b (patch)
tree00f9fda8709e47116bdb56082fd8837c84b09604
parent2bf760b9b4d71d3b67c5489d2c46bfa04d4629f9 (diff)
downloadgo-gg-pgx-8871cb8f2b8163d803f8f5ad2c2dc2a3f345bb0b.tar.gz
add test coverage
-rw-r--r--.gitignore1
-rw-r--r--handle.go2
-rw-r--r--interface.go28
-rw-r--r--interface_test.go183
4 files changed, 198 insertions, 16 deletions
diff --git a/.gitignore b/.gitignore
index 00d893e..d18b035 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/build/
!/build/.gitkeep
+/.testdb/
diff --git a/handle.go b/handle.go
index b3017ce..bd2462d 100644
--- a/handle.go
+++ b/handle.go
@@ -76,7 +76,7 @@ func (h poolHandle) GSQLPrepare(ctx context.Context, query string, repeated bool
conn.Release()
return err
}
- return wrappedPreparedStatement{ctx, stmt, h.inner, deallocate}, nil
+ return wrappedPreparedStatement{ctx, stmt, conn, deallocate}, nil
}
// GSQLQuery implements the [gsql.Handle] interface.
diff --git a/interface.go b/interface.go
index d4a5b56..b9d7def 100644
--- a/interface.go
+++ b/interface.go
@@ -16,8 +16,6 @@ import (
"go.xyrillian.de/gg/gsql"
)
-// TODO: test coverage (via gg/pgruntime)
-
// NOTE: The internal structure of these types follows the pattern established
// by (and explained in) `std.go` of `go.xyrillian.de/gg/gsql`.
@@ -35,13 +33,13 @@ func NewConn(conn *pgx.Conn) *Conn {
return &Conn{conn, connHandle{conn}}
}
-// Begin is like [pgx.Conn.Begin], but wraps the resulting transaction into a [Handle].
+// Begin is like [pgx.Conn.Begin], but wraps the resulting transaction into a [gsql.Handle].
func (conn *Conn) Begin(ctx context.Context) (*Tx, error) {
tx, err := conn.Conn.Begin(ctx)
return maybeNewTx(tx), err
}
-// BeginTx is like [pgx.Conn.BeginTx], but wraps the resulting transaction into a [Handle].
+// BeginTx is like [pgx.Conn.BeginTx], but wraps the resulting transaction into a [gsql.Handle].
func (conn *Conn) BeginTx(ctx context.Context, opts pgx.TxOptions) (*Tx, error) {
tx, err := conn.Conn.BeginTx(ctx, opts)
return maybeNewTx(tx), err
@@ -61,13 +59,13 @@ func NewPool(pool *pgxpool.Pool) *Pool {
return &Pool{pool, poolHandle{pool}}
}
-// Acquire is like [pgxpool.Pool.Acquire], but wraps the resulting connection into a [Handle].
+// Acquire is like [pgxpool.Pool.Acquire], but wraps the resulting connection into a [gsql.Handle].
func (pool *Pool) Acquire(ctx context.Context) (*PoolConn, error) {
conn, err := pool.Pool.Acquire(ctx)
return maybe(NewPoolConn, conn), err
}
-// AcquireAllIdle is like [pgxpool.Pool.AcquireAllIdle], but wraps the resulting connections into [Handle] instances.
+// AcquireAllIdle is like [pgxpool.Pool.AcquireAllIdle], but wraps the resulting connections into [gsql.Handle] instances.
func (pool *Pool) AcquireAllIdle(ctx context.Context) []*PoolConn {
conns := pool.Pool.AcquireAllIdle(ctx)
result := make([]*PoolConn, len(conns))
@@ -77,20 +75,20 @@ func (pool *Pool) AcquireAllIdle(ctx context.Context) []*PoolConn {
return result
}
-// AcquireFunc is like [pgxpool.Pool.AcquireFunc], but wraps the resulting connection into a [Handle].
+// AcquireFunc is like [pgxpool.Pool.AcquireFunc], but wraps the resulting connection into a [gsql.Handle].
func (pool *Pool) AcquireFunc(ctx context.Context, f func(*PoolConn) error) error {
return pool.Pool.AcquireFunc(ctx, func(conn *pgxpool.Conn) error {
return f(maybe(NewPoolConn, conn))
})
}
-// Begin is like [pgxpool.Pool.Begin], but wraps the resulting transaction into a [Handle].
+// Begin is like [pgxpool.Pool.Begin], but wraps the resulting transaction into a [gsql.Handle].
func (pool *Pool) Begin(ctx context.Context) (*Tx, error) {
tx, err := pool.Pool.Begin(ctx)
return maybeNewTx(tx), err
}
-// BeginTx is like [pgxpool.Pool.BeginTx], but wraps the resulting transaction into a [Handle].
+// BeginTx is like [pgxpool.Pool.BeginTx], but wraps the resulting transaction into a [gsql.Handle].
func (pool *Pool) BeginTx(ctx context.Context, opts pgx.TxOptions) (*Tx, error) {
tx, err := pool.Pool.BeginTx(ctx, opts)
return maybeNewTx(tx), err
@@ -110,26 +108,26 @@ func NewPoolConn(pool *pgxpool.Conn) *PoolConn {
return &PoolConn{pool, poolConnHandle{pool}}
}
-// Begin is like [pgxpool.Conn.Begin], but wraps the resulting transaction into a [Handle].
+// Begin is like [pgxpool.Conn.Begin], but wraps the resulting transaction into a [gsql.Handle].
func (conn *PoolConn) Begin(ctx context.Context) (*Tx, error) {
tx, err := conn.Conn.Begin(ctx)
return maybeNewTx(tx), err
}
-// BeginTx is like [pgxpool.Conn.BeginTx], but wraps the resulting transaction into a [Handle].
+// BeginTx is like [pgxpool.Conn.BeginTx], but wraps the resulting transaction into a [gsql.Handle].
func (conn *PoolConn) BeginTx(ctx context.Context, opts pgx.TxOptions) (*Tx, error) {
tx, err := conn.Conn.BeginTx(ctx, opts)
return maybeNewTx(tx), err
}
-// GetConn is like [pgxpool.Conn.Conn], but wraps the resulting connection into a [Handle].
+// GetConn is like [pgxpool.Conn.Conn], but wraps the resulting connection into a [gsql.Handle].
//
// This method should be called "Conn", but one of the embedded fields of this type blocks that name.
func (conn *PoolConn) GetConn() *Conn {
return maybe(NewConn, conn.Conn.Conn())
}
-// Hijack is like [pgxpool.Conn.Conn], but wraps the resulting connection into a [Handle].
+// Hijack is like [pgxpool.Conn.Conn], but wraps the resulting connection into a [gsql.Handle].
func (conn *PoolConn) Hijack() *Conn {
return maybe(NewConn, conn.Conn.Hijack())
}
@@ -143,12 +141,12 @@ type Tx struct {
gsql.Handle
}
-// NewTx wraps an instance of [pgx.Tx] into the [Tx] type that implements [Handle].
+// NewTx wraps an instance of [pgx.Tx] into the [Tx] type that implements [gsql.Handle].
func NewTx(tx pgx.Tx) *Tx {
return &Tx{tx, txHandle{tx}}
}
-// Conn is like the Conn() method of [pgx.Tx], but wraps the resulting connection into a [Handle].
+// Conn is like the Conn() method of [pgx.Tx], but wraps the resulting connection into a [gsql.Handle].
func (t *Tx) Conn() *Conn {
return maybe(NewConn, t.Tx.Conn())
}
diff --git a/interface_test.go b/interface_test.go
new file mode 100644
index 0000000..68f1dc2
--- /dev/null
+++ b/interface_test.go
@@ -0,0 +1,183 @@
+// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
+// SPDX-License-Identifier: Apache-2.0
+
+package gg_pgx_test
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "testing"
+
+ "go.xyrillian.de/gg/assert"
+ "go.xyrillian.de/gg/errext"
+ "go.xyrillian.de/gg/gsql"
+ "go.xyrillian.de/gg/pgruntime"
+
+ gg_pgx "go.xyrillian.de/gg-pgx"
+)
+
+// NOTE: There is not a lot of test coverage for the methods in `interface.go`, which is intentional:
+// These are just boilerplate implementations where, if it compiles at all, it is all but guaranteed to work as intended.
+
+var defaultBehavior pgruntime.ConnectionBehavior
+
+func TestMain(m *testing.M) {
+ pgruntime.WithTestDB(m, m.Run)
+}
+
+func TestConn(t *testing.T) {
+ conn, _ := gg_pgx.SingleConnector().ConnectForTest(t, defaultBehavior)
+ testConnectionHandle(t, conn)
+}
+
+func TestPool(t *testing.T) {
+ pool, _ := gg_pgx.PoolConnector().ConnectForTest(t, defaultBehavior)
+ testConnectionHandle(t, pool)
+}
+
+func TestPoolConn(t *testing.T) {
+ pool, _ := gg_pgx.PoolConnector().ConnectForTest(t, defaultBehavior)
+ t.Cleanup(pool.Close)
+
+ conn, err := pool.Acquire(t.Context())
+ if assert.ErrEqual(t, err, nil) {
+ testConnectionHandle(t, conn)
+ }
+}
+
+func testConnectionHandle[H gsql.ConnectionHandle](t *testing.T, conn H) {
+ ctx := t.Context()
+ t.Cleanup(func() {
+ assert.ErrEqual(t, conn.GSQLClose(ctx), nil)
+ })
+
+ // test GSQLPrepare() + Exec()
+ _, err := execQuery(ctx, conn, `CREATE TABLE people (id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL)`, nil)
+ if !assert.ErrEqual(t, err, nil) {
+ t.FailNow()
+ }
+ for _, name := range []string{"Alice", "Bob", "Carol"} {
+ _, err = execQuery(ctx, conn, `INSERT INTO people (name) VALUES ($1)`, []any{name})
+ if !assert.ErrEqual(t, err, nil) {
+ t.FailNow()
+ }
+ }
+
+ // test GSQLPrepare() + QueryRow()
+ var id int64
+ err = queryRow(ctx, conn, `SELECT id FROM people WHERE name = $1`, []any{"Bob"}, []any{&id})
+ if !assert.ErrEqual(t, err, nil) {
+ t.FailNow()
+ }
+ assert.Equal(t, id, 2)
+
+ // test GSQLQuery()
+ type person struct {
+ ID int64
+ Name string
+ }
+ var people []person
+ err = foreachRow(t, ctx, conn, `SELECT id, name FROM people ORDER BY id`, nil, []string{"id", "name"}, func(scan func(slots ...any) error) error {
+ var p person
+ err := scan(&p.ID, &p.Name)
+ if err != nil {
+ return err
+ }
+ people = append(people, p)
+ return nil
+ })
+ if !assert.ErrEqual(t, err, nil) {
+ t.FailNow()
+ }
+ assert.Equal(t, people, []person{
+ {1, "Alice"},
+ {2, "Bob"},
+ {3, "Carol"},
+ })
+
+ // test GSQLTransact() + success
+ err = conn.GSQLTransact(ctx, func(tx gsql.Handle) error {
+ _, err := execQuery(ctx, tx, `UPDATE people SET name = $1 WHERE id = $2`, []any{"Carolin", 3})
+ return err
+ })
+ if !assert.ErrEqual(t, err, nil) {
+ t.FailNow()
+ }
+
+ // check that the transaction was committed
+ var name string
+ err = queryRow(ctx, conn, `SELECT name FROM people WHERE id = $1`, []any{3}, []any{&name})
+ if !assert.ErrEqual(t, err, nil) {
+ t.FailNow()
+ }
+ assert.Equal(t, name, "Carolin")
+
+ // test GSQLTransact() + error
+ err = conn.GSQLTransact(ctx, func(tx gsql.Handle) error {
+ _, err := execQuery(ctx, tx, `UPDATE people SET name = $1 WHERE id = $2`, []any{"Allen", 1})
+ if err != nil {
+ return err
+ }
+
+ // check that the change is visible within the transaction
+ var name string
+ err = queryRow(ctx, tx, `SELECT name FROM people WHERE id = $1`, []any{1}, []any{&name})
+ if !assert.ErrEqual(t, err, nil) {
+ t.FailNow()
+ }
+ assert.Equal(t, name, "Allen")
+
+ return errors.New("this was a bad idea")
+ })
+ if !assert.ErrEqual(t, err, "this was a bad idea") {
+ t.FailNow()
+ }
+
+ // check that the transaction was *not* committed
+ err = queryRow(ctx, conn, `SELECT name FROM people WHERE id = $1`, []any{1}, []any{&name})
+ if !assert.ErrEqual(t, err, nil) {
+ t.FailNow()
+ }
+ assert.Equal(t, name, "Alice")
+}
+
+// Convenience function for executing a one-off SQL query returning no rows.
+func execQuery(ctx context.Context, db gsql.Handle, query string, args []any) (sql.Result, error) {
+ stmt, err := db.GSQLPrepare(ctx, query, false)
+ if err != nil {
+ return nil, err
+ }
+ result, err := stmt.Exec(ctx, args)
+ return result, errext.WithCleanup(err, "stmt.Close", stmt.Close())
+}
+
+// Convenience function for executing a one-off SQL query returning one row.
+func queryRow(ctx context.Context, db gsql.Handle, query string, args, slots []any) error {
+ stmt, err := db.GSQLPrepare(ctx, query, true) // repeated=true does not technically make sense, but provides code coverage
+ if err != nil {
+ return err
+ }
+ err = stmt.QueryRow(ctx, args, slots)
+ return errext.WithCleanup(err, "stmt.Close", stmt.Close())
+}
+
+// Convenience function for executing a one-off SQL query returning several rows.
+func foreachRow(t *testing.T, ctx context.Context, db gsql.Handle, query string, args []any, expectedColumnNames []string, action func(scan func(slots ...any) error) error) error {
+ t.Helper()
+ rows, err := db.GSQLQuery(ctx, query, args)
+ if err != nil {
+ return err
+ }
+ columns, err := rows.Columns()
+ if assert.ErrEqual(t, err, nil) {
+ assert.Equal(t, columns, expectedColumnNames)
+ }
+ for rows.Next() {
+ err := action(rows.Scan)
+ if err != nil {
+ return errext.WithCleanup(err, "rows.Close", rows.Close())
+ }
+ }
+ return rows.Close()
+}