pgmem

package
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: Apache-2.0, MIT Imports: 21 Imported by: 0

README

pgmem

pgmem is a process-local PostgreSQL emulator for fast Go tests. It ports the database-and-schema testing model popularized by oguimbal/pg-mem to an idiomatic Go API, with no PostgreSQL server. SQL is parsed into PostgreSQL's own AST through pg_query_go, then translated for the embedded execution engine; syntax is never classified with regular expressions.

database := pgmem.MustNew()
defer database.Close()

public := database.Public()
if err := public.None(`
  CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL
  )
`); err != nil {
  panic(err)
}

if err := public.None(`INSERT INTO users (name) VALUES ($1)`, "Ada"); err != nil {
  panic(err)
}

user, err := public.One(`SELECT id, name FROM users`)

The same database can back code written against database/sql:

pool := database.Open()
defer pool.Close()

ctx := context.Background()
var name string
err = pool.QueryRowContext(ctx,
  `SELECT name FROM users WHERE id = $1`,
  1,
).Scan(&name)

DB.Open uses the public schema. Use schema.Open when unqualified statements should target a named schema. Close every returned pool before closing the owning DB.

Backups are reusable restore points for the public schema and every named schema:

backup, err := database.Backup()
if err != nil {
  panic(err)
}

// Run a test that changes rows, then reset its data for the next case.
if err := backup.Restore(); err != nil {
  panic(err)
}

Data-only changes can be restored repeatedly. Restore returns ErrSchemaChanged if DDL or the named-schema topology changed after the backup, without modifying the current data. Context cancellation is honored while validating the restore; once its commit point starts, the already validated public and named-schema swap runs to completion without cancellation. Exceptional engine failures during that internal multi-schema swap are not a transactional rollback boundary.

The shape a test takes

Those three pieces — New, your real schema, Backup/Restore — compose into the pattern the suites here use. The schema comes from the migration files the service actually ships, because a hand-written CREATE TABLE in a _test.go is a second schema that drifts from the first one:

//go:embed migrations/*.up.sql
var migrations embed.FS

var (
	database *pgmem.DB
	clean    *pgmem.Backup
)

var _ = BeforeSuite(func() {
	database = pgmem.MustNew()

	entries, err := fs.Glob(migrations, "migrations/*.up.sql")
	Expect(err).NotTo(HaveOccurred())
	sort.Strings(entries)
	for _, entry := range entries {
		statement, err := migrations.ReadFile(entry)
		Expect(err).NotTo(HaveOccurred())
		Expect(database.Public().None(string(statement))).To(Succeed())
	}

	clean, err = database.Backup()
	Expect(err).NotTo(HaveOccurred())
})

var _ = AfterSuite(func() { Expect(database.Close()).To(Succeed()) })

// Every spec starts from the same empty tables, with no container to wait
// for, no port to allocate, and nothing to tear down between cases.
var _ = BeforeEach(func() { Expect(clean.Restore()).To(Succeed()) })

Restore is data-only by contract: it returns ErrSchemaChanged — without touching the current data — if DDL or the named-schema topology moved after the backup was taken, so a spec that quietly adds a table fails loudly rather than poisoning the ones after it.

Errors

Typed and comparable with errors.Is, so a test asserts the condition rather than a message:

Sentinel Returned when
ErrNoRows One expected a row and received none
ErrTooManyRows One received more than one
ErrRelationNotFound the requested table or view is not in the schema
ErrInvalidSchema the schema name is invalid or unknown
ErrUnsupported valid PostgreSQL syntax outside the documented compatibility surface
ErrSchemaChanged a backup was restored after DDL or the schema topology changed
ErrClosed the database was already closed

Execution failures additionally arrive as *pgmem.Error, which carries a PostgreSQL-style Code (SQLSTATE) and the Statement that produced it, with the underlying cause still reachable through errors.Is/errors.As.

Schema-local interceptors can supply ad-hoc results before parsing or execution:

subscription := public.InterceptQueries(func(
  ctx context.Context,
  statement string,
  arguments []any,
) (pgmem.Result, bool, error) {
  if statement != "current user" {
    return pgmem.Result{}, false, nil // pass through
  }
  return pgmem.Result{
    Rows: []pgmem.Row{{"name": "Ada"}},
  }, true, nil
})
defer subscription.Unsubscribe()

Interceptors run in registration order and the first handled result wins. They currently apply to Schema methods only, not calls made through DB.Open, Schema.Open, or DB.SQLDB.

Scalar functions can be registered after the database is already in use. A function registered on DB belongs to public; register on a Schema for a schema-local function:

err := database.RegisterFunction(pgmem.Function{
  Name:  "greet",
  Arity: 1,
  Implementation: func(arguments ...any) (any, error) {
    return "hello " + arguments[0].(string), nil
  },
})
if err != nil {
  panic(err)
}

row, err := public.One(`SELECT greet('Ada') AS greeting`)

Fixed arities may be overloaded; a variadic definition uses Arity as its minimum. Registrations and replacements are isolated per database and schema. Callback errors and panics become query errors instead of escaping through the embedded engine. Callbacks run synchronously inside the database's sole executor connection: they must be prompt and must not query, back up, restore, or close their owning database.

Compatibility target

The v0.1 contract covers the database/schema API, isolated schemas, typed errors, materialized query results, PostgreSQL AST/type translation, backups, query interception, schema-local scalar functions, raw table fixtures, and the translating database/sql adapter. See COMPATIBILITY.md for the exact tested surface and known gaps.

Like upstream pg-mem, this is a best-effort unit-test emulator rather than a production PostgreSQL replacement. Always validate migrations and integration behavior against the PostgreSQL versions your application supports.

Development

The module requires Go 1.26 and a C compiler because pg_query_go embeds the PostgreSQL parser through cgo. Behavior tests use Ginkgo v2 and Gomega; expectation-based interface mocks use gomock.

go test -race ./...
go generate ./...

License and origin

Licensed under the MIT License. This is an independent Go implementation inspired by pg-mem's documented behavior; see UPSTREAM.md for the compatibility and attribution boundary.

Documentation

Overview

Package pgmem provides a fast, process-local PostgreSQL emulator for Go tests. It follows pg-mem's database-and-schema model while exposing idiomatic context-aware Go APIs and a database/sql adapter.

Pgmem is an emulator, not PostgreSQL. Applications should still run their migrations and integration tests against every PostgreSQL version they support before release.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrClosed is returned after a database has been closed.
	ErrClosed = errors.New("pgmem: database is closed")
	// ErrNoRows is returned when One expects a row and receives none.
	ErrNoRows = errors.New("pgmem: expected one row, got none")
	// ErrTooManyRows is returned when One receives more than one row.
	ErrTooManyRows = errors.New("pgmem: expected one row, got many")
	// ErrInvalidSchema is returned for an invalid or unknown schema name.
	ErrInvalidSchema = errors.New("pgmem: invalid schema")
	// ErrUnsupported is returned when valid PostgreSQL syntax is outside the
	// emulator's documented compatibility surface.
	ErrUnsupported = errors.New("pgmem: unsupported PostgreSQL feature")
	// ErrRelationNotFound is returned when a requested table or view does not
	// exist in a schema.
	ErrRelationNotFound = errors.New("pgmem: relation does not exist")
	// ErrSchemaChanged is returned when restoring a backup after the database's
	// schema definitions or named-schema topology changed.
	ErrSchemaChanged = errors.New("pgmem: schema changed since backup")
)

Functions

This section is empty.

Types

type Backup

type Backup struct {
	// contains filtered or unexported fields
}

Backup is an immutable restore point owned by one DB. A backup can be restored repeatedly, including concurrently with ordinary operations.

Restore accepts data changes made after the backup, but rejects schema DDL and named-schema topology changes. That restriction matches pg-mem's backup contract and keeps existing Schema handles valid.

func (*Backup) Restore

func (b *Backup) Restore() error

Restore resets all data to this restore point.

func (*Backup) RestoreContext

func (b *Backup) RestoreContext(ctx context.Context) error

RestoreContext resets all data to this restore point. It is repeatable. If any schema definition or the named-schema topology changed since Backup, RestoreContext returns ErrSchemaChanged without changing data.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB is one isolated, in-memory database. A DB owns its SQL connection and must be closed when a test or process is finished with it.

func MustNew

func MustNew(options ...Option) *DB

MustNew creates a database or panics. It is intended for test setup.

func New

func New(options ...Option) (*DB, error)

New creates an isolated in-memory database.

func NewContext

func NewContext(ctx context.Context, options ...Option) (*DB, error)

NewContext creates an isolated in-memory database and validates its connection before returning.

func (*DB) Backup

func (db *DB) Backup() (*Backup, error)

Backup creates a restore point for the public schema and every named schema.

func (*DB) BackupContext

func (db *DB) BackupContext(ctx context.Context) (*Backup, error)

BackupContext creates a restore point for the public schema and every named schema. The returned backup owns defensive copies of all database images.

func (*DB) Close

func (db *DB) Close() error

Close releases all memory owned by the database. It is idempotent.

func (*DB) Connector

func (db *DB) Connector() driver.Connector

Connector returns a database/sql connector for the public schema.

The returned connector does not own DB. Close the database/sql pool before closing DB.

func (*DB) CreateSchema

func (db *DB) CreateSchema(ctx context.Context, name string) (*Schema, error)

CreateSchema creates and returns an isolated named schema.

func (*DB) Open

func (db *DB) Open() *sql.DB

Open returns a database/sql pool backed by the public schema. The returned pool and DB have independent lifetimes and both must be closed.

func (*DB) Public

func (db *DB) Public() *Schema

Public returns the default public schema.

func (*DB) RegisterFunction

func (db *DB) RegisterFunction(function Function) error

RegisterFunction registers or atomically replaces one public-schema scalar function overload. Registering functions on separate DB values is isolated.

func (*DB) SQLDB

func (db *DB) SQLDB() *sql.DB

SQLDB exposes the underlying database/sql pool. Direct calls currently use the internal dialect; prefer Schema methods when PostgreSQL translation is required.

func (*DB) Schema

func (db *DB) Schema(name string) (*Schema, bool)

Schema returns a named schema and whether it exists.

func (*DB) SchemaNames

func (db *DB) SchemaNames() []string

SchemaNames returns a stable snapshot of known schema names.

type Error

type Error struct {
	Code      string
	Message   string
	Statement string
	Cause     error
}

Error adds a PostgreSQL-style SQLSTATE code and statement context to an execution error. Cause remains available through errors.Is and errors.As.

func (*Error) Error

func (e *Error) Error() string

func (*Error) SQLState

func (e *Error) SQLState() string

SQLState returns the five-character PostgreSQL error code.

func (*Error) Unwrap

func (e *Error) Unwrap() error

type Function

type Function struct {
	Name           string
	Arity          int
	Variadic       bool
	Implementation ScalarFunction
}

Function describes one scalar function overload. Arity is the exact number of positional arguments for a fixed function and the minimum number for a variadic function.

type ITranslator

type ITranslator interface {
	Translate(ctx context.Context, defaultSchema, statement string) (string, error)
}

ITranslator converts PostgreSQL syntax into the internal execution dialect. Implementations must be safe for concurrent use.

type Option

type Option func(cfg *config) error

Option configures a database before its first connection is opened.

func WithTranslator

func WithTranslator(translator ITranslator) Option

WithTranslator replaces the PostgreSQL compatibility translator. This is primarily useful for adding project-specific syntax during tests.

type QueryInterceptor

type QueryInterceptor func(ctx context.Context, statement string, arguments []any) (result Result, handled bool, err error)

QueryInterceptor can replace a statement with an ad-hoc result. Returning handled=false passes the original statement to the next interceptor and, eventually, the embedded SQL engine.

Interceptors receive the original PostgreSQL statement and a defensive copy of its arguments. They run in registration order, and the first handled result wins.

type Result

type Result struct {
	Rows     []Row
	RowCount int64
	Command  string
}

Result contains materialized rows and statement metadata.

type Row

type Row map[string]any

Row is one result row keyed by column label.

type ScalarFunction

type ScalarFunction func(arguments ...any) (any, error)

ScalarFunction implements a registered scalar SQL function. Arguments use the embedded engine's scalar representations: int64, float64, string, []byte, and nil. The returned value must be one of those types or an ordinary Go integer or float. bool and time.Time returns are accepted but the embedded engine determines how they are materialized.

type Schema

type Schema struct {
	// contains filtered or unexported fields
}

Schema is a namespace inside a DB.

func (*Schema) Connector

func (s *Schema) Connector() driver.Connector

Connector returns a database/sql connector whose unqualified statements use this schema. The returned connector does not own the schema's DB.

func (*Schema) Exec

func (s *Schema) Exec(statement string, arguments ...any) (Result, error)

Exec executes a statement and returns its affected-row count.

func (*Schema) ExecContext

func (s *Schema) ExecContext(ctx context.Context, statement string, arguments ...any) (Result, error)

ExecContext executes a statement and returns its affected-row count.

func (*Schema) GetTable

func (s *Schema) GetTable(name string) (*Table, error)

GetTable is the pg-mem-compatible spelling of Table.

func (*Schema) InterceptQueries

func (s *Schema) InterceptQueries(interceptor QueryInterceptor) *Subscription

InterceptQueries registers an interceptor for this schema. Interceptors are schema-local: registering one on public does not affect a named schema.

InterceptQueries panics when interceptor is nil, matching the usual Go registration contract for callbacks that cannot be useful when nil.

func (*Schema) Many

func (s *Schema) Many(statement string, arguments ...any) ([]Row, error)

Many executes a query and returns every row.

func (*Schema) ManyContext

func (s *Schema) ManyContext(ctx context.Context, statement string, arguments ...any) ([]Row, error)

ManyContext executes a query and returns every row.

func (*Schema) Name

func (s *Schema) Name() string

Name returns the schema name.

func (*Schema) None

func (s *Schema) None(statement string, arguments ...any) error

None executes a statement that is not expected to return rows.

func (*Schema) NoneContext

func (s *Schema) NoneContext(ctx context.Context, statement string, arguments ...any) error

NoneContext executes a statement that is not expected to return rows.

func (*Schema) One

func (s *Schema) One(statement string, arguments ...any) (Row, error)

One executes a query that must return exactly one row.

func (*Schema) OneContext

func (s *Schema) OneContext(ctx context.Context, statement string, arguments ...any) (Row, error)

OneContext executes a query that must return exactly one row.

func (*Schema) Open

func (s *Schema) Open() *sql.DB

Open returns a database/sql pool whose unqualified statements use this schema. Close the pool before closing its DB.

func (*Schema) Query

func (s *Schema) Query(statement string, arguments ...any) (Result, error)

Query executes and fully materializes a row-returning statement.

func (*Schema) QueryContext

func (s *Schema) QueryContext(ctx context.Context, statement string, arguments ...any) (Result, error)

QueryContext executes and fully materializes a row-returning statement.

func (*Schema) RegisterFunction

func (s *Schema) RegisterFunction(function Function) error

RegisterFunction registers or atomically replaces one scalar function overload in this schema. Fixed overloads are selected before a matching variadic overload.

func (*Schema) Table

func (s *Schema) Table(name string) (*Table, error)

Table returns a checked handle to a table or view in the schema.

func (*Schema) TableContext

func (s *Schema) TableContext(ctx context.Context, name string) (*Table, error)

TableContext returns a checked handle to a table or view in the schema.

type Subscription

type Subscription struct {
	// contains filtered or unexported fields
}

Subscription controls one query-interceptor registration.

func (*Subscription) Unsubscribe

func (s *Subscription) Unsubscribe()

Unsubscribe removes the interceptor. It is safe to call more than once and concurrently with query execution. An invocation already in progress may finish.

type Table

type Table struct {
	// contains filtered or unexported fields
}

Table exposes a small raw-row API for test setup and inspection. SQL remains the primary interface; Table is useful when a fixture is clearer as Go data.

func (*Table) Find

func (t *Table) Find(template Row) ([]Row, error)

Find returns rows whose columns equal every non-nil template entry. A nil template value matches SQL NULL.

func (*Table) FindContext

func (t *Table) FindContext(ctx context.Context, template Row) ([]Row, error)

FindContext returns rows whose columns equal every template entry.

func (*Table) Insert

func (t *Table) Insert(row Row) (Row, error)

Insert inserts a raw row and returns the stored row after defaults and generated values have been applied.

func (*Table) InsertContext

func (t *Table) InsertContext(ctx context.Context, row Row) (Row, error)

InsertContext inserts a raw row and returns the stored row after defaults and generated values have been applied.

func (*Table) Name

func (t *Table) Name() string

Name returns the unqualified relation name.

func (*Table) Schema

func (t *Table) Schema() *Schema

Schema returns the table's owning schema.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL