aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-08-05 22:06:47 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-08-05 22:06:48 +0200
commit3e9488e22d43cfba0b9306cf916f95fd833fcd97 (patch)
tree97e9dc38cd262c71fa5ba826523b73d4b82c0021
parentbaab5f6fe0aafa7752f8e9ae86fc966afc18fe1a (diff)
downloadgo-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.go66
-rw-r--r--pgruntime/behavior.go15
-rw-r--r--pgruntime/connector.go9
-rw-r--r--pgruntime/helpers.go60
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{&currentVersion, &dirty})
+ err = gq.QueryRow(ctx, db, `SELECT version, dirty FROM schema_migrations`, nil, []any{&currentVersion, &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 {