storelib

package module
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2025 License: MIT Imports: 8 Imported by: 2

README

StoreLib

StoreLib provides a multi-database Store implementation, which currently supports Postgres and SQLite backends.

Using

Add storelib to your project:

go get github.com/dogeorg/storelib

Example

Use the following template to start your Store implementation. The comments here will guide your changes:

package store

import (
	"context"

	"github.com/dogeorg/my-new-project/spec" // import your 'spec' package, see below
	"github.com/dogeorg/storelib"
)


// First some type aliases,

// These interfaces define your Store API
type Store = spec.Store
type StoreTx = spec.StoreTx

// These make the code readable
type StoreBase = storelib.StoreBase[Store, StoreTx]
type StoreImpl = storelib.StoreImpl[Store, StoreTx]


// Now define the store,

type MyStore struct {
	StoreBase // pull in the storelib

    // add any fields you need here
}

var _ Store = &MyStore{} // interface assertion


// Create function,

func NewStore(connString string, ctx context.Context) (Store, error) {
	store := &MyStore{}
    // fails if connString is bad, or if schema (migrations) are bad.
	err := storelib.InitStore(store, &store.StoreBase, connString, MIGRATIONS, ctx)
    // initialise your custom fields here (or above)
	return store, err
}


// Clone function, to make transactions work,

func (s *MyStore) Clone() (StoreImpl, *StoreBase, Store, StoreTx) {
	newstore := &MyStore{}
    // copy your custom fields here
	return newstore, &newstore.StoreBase, newstore, newstore
}


// DATABASE SCHEMA

const SCHEMA_v1 = `

    CREATE TABLE ...

`

var MIGRATIONS = []storelib.Migration{
	{Version: 1, SQL: SCHEMA_v1},

    // add DB migrations here as your schema changes
}


// STORE METHODS

// Implement your Store API methods here.
// They will run inside a transaction if called inside the Transact() method,
// otherwise they will run outside of a transaction.

Your Store API interfaces also need to mention the storelib:

package spec

import (
	"github.com/dogeorg/storelib"
)

// Methods you can use inside a Transaction
type StoreTx interface {

	// Declare your Store API methods here

}

// Methods on the main Store
type Store interface {
	storelib.StoreAPI[Store, StoreTx] // include the Base Store API
	StoreTx                           // include all the StoreTx methods

    // By including `StoreTx` here, the StoreTx methods are also callable
    // directly on the Store outside a transaction. It's optional.

    // Declare any other Store API methods here.
}

The storelib.StoreAPI adds the following methods to the Store:

	// Transact performs a transactional update (with retries on conflict)
	Transact(fn func(tx StoreTx) error) error

	// WithCtx returns a new Store, bound to a specific cancellable Context
	WithCtx(ctx context.Context) Store

	// Close closes the database.
	Close()

Transact clones the Store so it can swap out the TX field, which allows your method implementations to work both inside and outside a transaction.

All of your Store implementation methods should use s.TX.Query and s.T.QueryRow so they will work both inside and outside a transaction.

Example Store Method

func (s *MyStore) GetFooBarBaz(name string) (id int64, bar string, err error) {
	row := s.TX.QueryRow("SELECT id,bar FROM fooz WHERE name=$1", name)
	err = row.Scan(&id, &bar)
	if err != nil {
		// this will return ErrNotFound or use the error message
		return 0, "", s.DBErr(err, "GetFooBarBaz: error scanning row")
	}
	return
}

The $1 style placeholders are compatible with both Postgres and SQLite.

Documentation

Index

Constants

View Source
const VERSION_SCHEMA string = `
CREATE TABLE IF NOT EXISTS migration (
	version INTEGER NOT NULL DEFAULT 1
);
`

Variables

View Source
var ErrAlreadyExists = errors.New("already-exists")
View Source
var ErrConflict = errors.New("db-conflict")
View Source
var ErrNotFound = errors.New("not-found")

Functions

func InitStore

func InitStore[Store any, StoreTx any](impl StoreImpl[Store, StoreTx], base *StoreBase[Store, StoreTx], connString string, migrations []Migration, ctx context.Context) error

InitStore initialises a Store that uses Postgres or SQLite

Types

type FnInitAppData

type FnInitAppData func(*sql.Tx) error

FnInitAppData is an optional callback to run after the migration, within the same transaction.

type Migration

type Migration struct {
	Version int           // Monotonically increasing schema version
	SQL     string        // Query to uprade the schema
	Update  FnInitAppData // Callback to run after the migration (can be nil)
}

Migration is a schema migration to pass to InitStoreLib

type Queryable

type Queryable interface {
	Exec(query string, args ...any) (sql.Result, error)
	Query(query string, args ...any) (*sql.Rows, error)
	QueryRow(query string, args ...any) *sql.Row
	Prepare(query string) (*sql.Stmt, error)
}

The common read-only parts of sql.DB and sql.Tx interfaces

type StoreAPI

type StoreAPI[Store any, StoreTx any] interface {

	// Transact performs a transactional update function
	Transact(fn func(txn StoreTx) error) error

	// WithCtx returns the same Store interface, bound to a specific cancellable Context
	WithCtx(ctx context.Context) Store

	// Close closes the database.
	Close()
}

StoreAPI must be included in your Store interface, along with your public Store methods.

type StoreBase

type StoreBase[Store any, StoreTx any] struct {
	Txn   Queryable // Query the DB inside/outside a Transaction
	RawDB *sql.DB   // Bypass Txn and directly access the DB
	Ctx   context.Context
	// contains filtered or unexported fields
}

StoreBase must be included in your Store imeplentation struct

func (*StoreBase[Store, StoreTx]) Close

func (s *StoreBase[Store, StoreTx]) Close()

Close closes the connection to the store for clean shutdown.

func (StoreBase[Store, StoreTx]) DBErr

func (s StoreBase[Store, StoreTx]) DBErr(err error, where string) error

func (*StoreBase[Store, StoreTx]) IsConflict

func (s *StoreBase[Store, StoreTx]) IsConflict(err error) bool

func (*StoreBase[Store, StoreTx]) Transact

func (s *StoreBase[Store, StoreTx]) Transact(fn func(tx StoreTx) error) error

Transact runs `fn` inside a transaction, retrying on DB conflicts.

func (*StoreBase[Store, StoreTx]) WithCtx

func (s *StoreBase[Store, StoreTx]) WithCtx(ctx context.Context) Store

WithCtx returns the same Store interface, bound to a specific cancellable Context.

type StoreImpl

type StoreImpl[Store any, StoreTx any] interface {
	// Clone must make a new copy of the Store implementation,
	// returning the new copy and a pointer to the StoreBase inside the new copy.
	Clone() (StoreImpl[Store, StoreTx], *StoreBase[Store, StoreTx], Store, StoreTx)
}

Jump to

Keyboard shortcuts

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