diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-08-05 22:06:47 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-08-05 22:06:48 +0200 |
| commit | 3e9488e22d43cfba0b9306cf916f95fd833fcd97 (patch) | |
| tree | 97e9dc38cd262c71fa5ba826523b73d4b82c0021 | |
| parent | baab5f6fe0aafa7752f8e9ae86fc966afc18fe1a (diff) | |
| download | go-gg-3e9488e22d43cfba0b9306cf916f95fd833fcd97.tar.gz | |
move pgruntime internal helpers to internal/gq
I want to use these in package pgtest.
| -rw-r--r-- | internal/gq/gq.go | 66 | ||||
| -rw-r--r-- | pgruntime/behavior.go | 15 | ||||
| -rw-r--r-- | pgruntime/connector.go | 9 | ||||
| -rw-r--r-- | pgruntime/helpers.go | 60 |
4 files changed, 79 insertions, 71 deletions
diff --git a/internal/gq/gq.go b/internal/gq/gq.go new file mode 100644 index 0000000..0eb2b0c --- /dev/null +++ b/internal/gq/gq.go @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> +// SPDX-License-Identifier: Apache-2.0 + +// Package gq contains helpers for gsql ("gsql query" -> gq). +// TODO: most of these functions should be either in gsql as public API, or in Oblast +package gq + +import ( + "context" + "database/sql" + + "go.xyrillian.de/gg/errext" + "go.xyrillian.de/gg/gsql" +) + +// ExecQuery is a 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()) +} + +// QueryRow is a 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, false) + if err != nil { + return err + } + err = stmt.QueryRow(ctx, args, slots) + return errext.WithCleanup(err, "stmt.Close", stmt.Close()) +} + +// SelectOneValue is a convenience function for executing a one-off SQL query returning one value. +func SelectOneValue[T any](ctx context.Context, db gsql.Handle, query string, args ...any) (T, error) { + stmt, err := db.GSQLPrepare(ctx, query, false) + if err != nil { + var none T + return none, err + } + + var result T + err = stmt.QueryRow(ctx, args, []any{&result}) + return result, errext.WithCleanup(err, "stmt.Close", stmt.Close()) +} + +// SelectSeveralValues is a convenience function for executing a one-off SQL query returning several single-column rows. +func SelectSeveralValues[T any](ctx context.Context, db gsql.Handle, query string, args ...any) ([]T, error) { + rows, err := db.GSQLQuery(ctx, query, args) + if err != nil { + return nil, err + } + var result []T + for rows.Next() { + // TODO: this should share growRecordSlice() from Oblast to optimize allocations + var value T + err := rows.Scan(&value) + if err != nil { + return nil, errext.WithCleanup(err, "rows.Close", rows.Close()) + } + result = append(result, value) + } + return result, errext.WithCleanup(nil, "rows.Err", rows.Err()) +} diff --git a/pgruntime/behavior.go b/pgruntime/behavior.go index 03ff338..debdef4 100644 --- a/pgruntime/behavior.go +++ b/pgruntime/behavior.go @@ -9,6 +9,7 @@ import ( "slices" "go.xyrillian.de/gg/gsql" + "go.xyrillian.de/gg/internal/gq" ) // ConnectionBehavior contains configuration for [Connector.Connect] and [Connector.ConnectForTest]. @@ -62,13 +63,13 @@ func (b ConnectionBehavior) applyTo(ctx context.Context, db gsql.ConnectionHandl func applyMigrations(ctx context.Context, db gsql.ConnectionHandle, migrations map[int64]string) error { // apply schema_migrations table schema - _, err := execQuery(ctx, db, MigrationsSchema, nil) + _, err := gq.ExecQuery(ctx, db, MigrationsSchema, nil) if err != nil { return fmt.Errorf("could not apply schema_migrations table schema: %w", err) } // read schema_migrations table - rowCount, err := selectOneValue[int64](ctx, db, `SELECT COUNT(*) FROM schema_migrations`) + rowCount, err := gq.SelectOneValue[int64](ctx, db, `SELECT COUNT(*) FROM schema_migrations`) if err != nil { return fmt.Errorf("could not check row count for schema_migrations: %w", err) } @@ -79,12 +80,12 @@ func applyMigrations(ctx context.Context, db gsql.ConnectionHandle, migrations m switch rowCount { case 0: currentVersion = 0 - _, err = execQuery(ctx, db, `INSERT INTO schema_migrations (version, dirty) VALUES (0, FALSE)`, nil) + _, err = gq.ExecQuery(ctx, db, `INSERT INTO schema_migrations (version, dirty) VALUES (0, FALSE)`, nil) if err != nil { return fmt.Errorf("could not initialize schema_migrations record: %w", err) } case 1: - err = queryRow(ctx, db, `SELECT version, dirty FROM schema_migrations`, nil, []any{¤tVersion, &dirty}) + err = gq.QueryRow(ctx, db, `SELECT version, dirty FROM schema_migrations`, nil, []any{¤tVersion, &dirty}) if err != nil { return fmt.Errorf("could not read schema_migrations record: %w", err) } @@ -112,7 +113,7 @@ func applyMigrations(ctx context.Context, db gsql.ConnectionHandle, migrations m err := db.GSQLTransact(ctx, func(tx gsql.Handle) error { // ensure that nobody else is migrating until we are done var actualVersion int64 - err := queryRow(ctx, db, `SELECT version FROM schema_migrations FOR UPDATE`, nil, []any{&actualVersion}) + err := gq.QueryRow(ctx, db, `SELECT version FROM schema_migrations FOR UPDATE`, nil, []any{&actualVersion}) if err != nil { return fmt.Errorf("could not obtain lock for schema migration: %w", err) } @@ -125,11 +126,11 @@ func applyMigrations(ctx context.Context, db gsql.ConnectionHandle, migrations m } // perform the next migration - _, err = execQuery(ctx, db, migrations[version], nil) + _, err = gq.ExecQuery(ctx, db, migrations[version], nil) if err != nil { return fmt.Errorf("could not execute schema migration: %w", err) } - _, err = execQuery(ctx, db, `UPDATE schema_migrations SET version = $1, dirty = FALSE`, []any{version}) + _, err = gq.ExecQuery(ctx, db, `UPDATE schema_migrations SET version = $1, dirty = FALSE`, []any{version}) if err != nil { return fmt.Errorf("could not update schema_migrations record: %w", err) } diff --git a/pgruntime/connector.go b/pgruntime/connector.go index 5276a26..76a45f1 100644 --- a/pgruntime/connector.go +++ b/pgruntime/connector.go @@ -15,6 +15,7 @@ import ( "go.xyrillian.de/gg/assert" "go.xyrillian.de/gg/errext" "go.xyrillian.de/gg/gsql" + "go.xyrillian.de/gg/internal/gq" ) // Connector describes how to connect to a PostgreSQL database given a [libpq-style connection URI]. @@ -135,14 +136,14 @@ func (c Connector[T]) ConnectForTest(t assert.TestingTB, behavior ConnectionBeha func createDatabaseIfMissing(ctx context.Context, db gsql.Handle, dbName string) error { // check if database exists - exists, err := selectOneValue[bool](ctx, db, `SELECT COUNT(*) > 0 FROM pg_catalog.pg_database WHERE datname = $1`, dbName) + exists, err := gq.SelectOneValue[bool](ctx, db, `SELECT COUNT(*) > 0 FROM pg_catalog.pg_database WHERE datname = $1`, dbName) if err != nil { return fmt.Errorf("while reading from pg_catalog.pg_database: %w", err) } // create database if necessary if !exists { - _, err = execQuery(ctx, db, "CREATE DATABASE "+quoteIdentifier(dbName), nil) + _, err = gq.ExecQuery(ctx, db, "CREATE DATABASE "+quoteIdentifier(dbName), nil) if err != nil { return fmt.Errorf("during CREATE DATABASE: %w", err) } @@ -158,7 +159,7 @@ func resetTestDatabase(ctx context.Context, db gsql.Handle, params testSetupPara condition += ` AND table_name != 'schema_migrations'` } query := fmt.Sprintf(`SELECT quote_ident(table_name) FROM information_schema.tables WHERE %s ORDER BY table_name`, condition) - quotedTableNames, err := selectSeveralValues[string](ctx, db, query) + quotedTableNames, err := gq.SelectSeveralValues[string](ctx, db, query) if err != nil { return fmt.Errorf("while listing tables to truncate: %w", err) } @@ -166,7 +167,7 @@ func resetTestDatabase(ctx context.Context, db gsql.Handle, params testSetupPara // truncate all tables at once if len(quotedTableNames) > 0 { query = fmt.Sprintf(`TRUNCATE %s RESTART IDENTITY CASCADE`, strings.Join(quotedTableNames, ", ")) - _, err = execQuery(ctx, db, query, nil) + _, err = gq.ExecQuery(ctx, db, query, nil) if err != nil { return fmt.Errorf("during %s: %w", query, err) } diff --git a/pgruntime/helpers.go b/pgruntime/helpers.go index 1019e6e..d68b920 100644 --- a/pgruntime/helpers.go +++ b/pgruntime/helpers.go @@ -4,69 +4,9 @@ package pgruntime import ( - "context" - "database/sql" "strings" - - "go.xyrillian.de/gg/errext" - "go.xyrillian.de/gg/gsql" ) -// Convenience function for executing a one-off SQL query returning no rows. -// TODO: move to gsql -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, false) - 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 one value. -// TODO: move to gsql -func selectOneValue[T any](ctx context.Context, db gsql.Handle, query string, args ...any) (T, error) { - stmt, err := db.GSQLPrepare(ctx, query, false) - if err != nil { - var none T - return none, err - } - - var result T - err = stmt.QueryRow(ctx, args, []any{&result}) - return result, errext.WithCleanup(err, "stmt.Close", stmt.Close()) -} - -// Convenience function for executing a one-off SQL query returning several single-column rows. -// TODO: move to gsql (and also add ForeachValue with a callback instead of a slice return, maybe even ForeachPair and ForeachTriple) -func selectSeveralValues[T any](ctx context.Context, db gsql.Handle, query string, args ...any) ([]T, error) { - rows, err := db.GSQLQuery(ctx, query, args) - if err != nil { - return nil, err - } - var result []T - for rows.Next() { - // TODO: this should share growRecordSlice() from Oblast to optimize allocations - var value T - err := rows.Scan(&value) - if err != nil { - return nil, errext.WithCleanup(err, "rows.Close", rows.Close()) - } - result = append(result, value) - } - return result, errext.WithCleanup(nil, "rows.Err", rows.Err()) -} - // Convenience function for preparing an identifier that needs to be inserted into a query verbatim // (e.g. a database name for CREATE DATABASE). func quoteIdentifier(name string) string { |
