diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | handle.go | 2 | ||||
| -rw-r--r-- | interface.go | 28 | ||||
| -rw-r--r-- | interface_test.go | 183 |
4 files changed, 198 insertions, 16 deletions
@@ -1,2 +1,3 @@ /build/ !/build/.gitkeep +/.testdb/ @@ -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() +} |
