pstore

package module
v0.0.0-...-0d909ac Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 3 Imported by: 0

README

go-ruby-pstore/pstore

pstore — go-ruby-pstore

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the transaction engine at the heart of Ruby's PStore — MRI 4.0.5's transactional, Marshal-backed object store (the pstore-0.2.1 gem). It runs the load → transaction-body → commit/abort state machine over a Hash "table" and serialises that table with go-ruby-marshal, so the on-disk bytes are byte-compatible with a file written by MRI's PStorewithout any Ruby runtime.

It is the PStore backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-marshal, go-ruby-yaml, go-ruby-regexp, and go-ruby-erb.

What it is — and isn't. The transactional core — load the table, run the body, commit on normal exit (skipping the write when nothing changed), abort or commit early, forbid writes in a read-only transaction, refuse a nested transaction, raise outside one — is fully deterministic and needs no interpreter, so it lives here as pure Go over the Marshal codec. The file half — opening the store file (O_CREAT), flock(LOCK_SH/LOCK_EX), the atomic-rename / rewind-truncate save strategies — is the host's job, injected as a two-method Backend. rbgo wires the real os.File + syscall.Flock; tests use an in-memory backend, so the whole suite is deterministic and Ruby-free.

Features

Faithful port of PStore's transaction semantics, validated against the ruby binary on every supported platform:

  • Commit on normal exit — a read-write transaction whose body returns normally Marshal-dumps the table back through the backend, only if the bytes changed (MRI's checksum/size guard); an unchanged transaction performs no write.
  • Commit / Abort early exit — return t.Commit() or t.Abort() from the body to exit it early (MRI's throw :pstore_abort_transaction); Abort discards every change, Commit persists the work so far. The body does not continue past either.
  • Read-only transactionsSet and Delete raise PStore::Error ("in read-only transaction") and the backend is never written.
  • The error taxonomy — a single *pstore.Error (Ruby's PStore::Error) with MRI's exact messages: "not in transaction", "in read-only transaction", "nested transaction", "undefined key '…'", and "PStore file seems to be corrupted.".
  • The on-disk Marshal format — the table is Marshal.dump/loaded via go-ruby-marshal, so a file this engine commits is read back unchanged by MRI's PStore, and vice-versa (a real ruby -rpstore file loads here).

CGO-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three OSes (Linux, macOS, Windows).

Install

go get github.com/go-ruby-pstore/pstore

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-marshal/marshal"
	"github.com/go-ruby-pstore/pstore"
)

func main() {
	// The host supplies the file seam (real os.File + flock in rbgo); here an
	// in-memory backend stands in.
	be := &mem{}
	store := pstore.New(be)

	// A read-write transaction: writes commit on normal return.
	store.Transaction(false, func(t *pstore.Tx) error {
		t.Set(marshal.Symbol("foo"), marshal.NewInt(0))
		t.Set(marshal.NewString("baz"), &marshal.Array{
			Elems: []marshal.Value{marshal.NewInt(1), marshal.NewInt(2)},
		})
		return nil // commit
	})

	// A read-only transaction: reads only; a write here would raise PStore::Error.
	store.Transaction(true, func(t *pstore.Tx) error {
		roots, _ := t.Roots()           // [:foo, "baz"]
		v, ok, _ := t.Get(marshal.Symbol("foo"))
		fmt.Println(roots, v, ok)
		return nil
	})
}

type mem struct{ b []byte }

func (m *mem) Load() ([]byte, error)  { return m.b, nil }
func (m *mem) Store(b []byte) error   { m.b = b; return nil }

API

// New returns a Store over an injected Backend.
func New(b Backend) *Store

// Backend is the file seam the host injects (real os.File + flock in rbgo).
type Backend interface {
	Load() ([]byte, error)   // current store bytes (empty == fresh store)
	Store(data []byte) error // overwrite with the marshalled table
}

// Transaction loads the table, runs body, and — on a committing read-write
// transaction whose table changed — Marshal-dumps it back to the backend.
// body returns t.Commit()/t.Abort() to exit early, any other error to propagate
// (no commit), or nil to finish (a read-write transaction then commits).
func (s *Store) Transaction(readOnly bool, body func(t *Tx) error) error

// In-transaction API (mirrors PStore#[], #[]=, #delete, #fetch, #roots/#keys,
// #root?/#key?, #commit, #abort). Keys and values are go-ruby-marshal Values.
func (t *Tx) Get(key marshal.Value) (marshal.Value, bool, error)            // PStore#[]
func (t *Tx) Set(key, val marshal.Value) error                             // PStore#[]=
func (t *Tx) Delete(key marshal.Value) (marshal.Value, error)              // PStore#delete
func (t *Tx) Fetch(key marshal.Value, def marshal.Value) (marshal.Value, error) // PStore#fetch
func (t *Tx) Roots() ([]marshal.Value, error)                              // PStore#roots
func (t *Tx) RootQ(key marshal.Value) (bool, error)                        // PStore#root?
func (t *Tx) Commit() error                                                // PStore#commit
func (t *Tx) Abort() error                                                 // PStore#abort

// Error is PStore::Error.
type Error struct{ /* … */ }

The table's keys and values are go-ruby-marshal Values (marshal.Symbol, *marshal.Str, marshal.Int, *marshal.Array, *marshal.Hash, …) — the same typed model the rest of the go-embedded-ruby stack speaks, so a host binds its own Ruby objects to and from this engine exactly as it does for Marshal.

Tests & coverage

The suite pairs deterministic, ruby-free tests over the in-memory backend (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential MRI oracle: it writes a store with the real ruby -rpstore, loads its file bytes here, and round-trips the table both directions (MRI→Go and Go→MRI), plus replays MRI's own commit / abort / read-only sequence and asserts the engine produces the identical persisted roots. The oracle scripts $stdout.binmode so Windows text-mode never corrupts the raw Marshal bytes, and skip themselves where ruby is absent.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-pstore/pstore authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package pstore is a pure-Go (no cgo) reimplementation of the transaction engine at the heart of Ruby's PStore — MRI 4.0.5's transactional, Marshal-backed object store (the pstore-0.2.1 gem).

PStore persists a Hash (the "table") of Ruby objects to a file using Marshal. Reads and writes happen inside a transaction; on normal exit a read-write transaction commits the table back to the file (unless Abort was called), while a read-only transaction never writes. This package implements exactly that transactional, Marshal-(de)serialising core — independent of any interpreter — over two injected seams:

  • a Backend (Load []byte / Store []byte): the bytes of the store file. The real File IO, O_CREAT, flock(LOCK_SH/LOCK_EX) and the atomic-rename / fast (rewind+truncate) save strategies are the host's job (rbgo wires real os.File + syscall.Flock); tests use an in-memory backend so the suite is fully deterministic and Ruby-free.
  • the go-ruby-marshal codec, so the on-disk bytes are byte-compatible with a file written by MRI's PStore (Marshal.dump of the table Hash).

The table's keys and values are go-ruby-marshal Values (the same typed value model the rest of the go-embedded-ruby stack speaks), so a host binds its own Ruby objects to and from this model exactly as it does for Marshal itself.

What it is — and isn't

The transaction state machine (load → run body → commit/abort, the read-only write guard, the not-in-transaction guard, the nested-transaction guard, the commit-only-on-change Marshal round-trip) is fully deterministic and needs no interpreter, so it lives here. The File open/flock/rename and the block's Ruby control flow (commit/abort exit the block via throw/catch in MRI) are the host's concern: this library exposes Commit and Abort as sentinel errors a host returns (or the body returns) to drive the same early-exit semantics.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Backend

type Backend interface {
	// Load returns the current store bytes. An empty slice means a fresh store
	// (MRI treats an empty / newly-created file as the empty table {}).
	Load() ([]byte, error)
	// Store overwrites the store bytes with the marshalled table.
	Store(data []byte) error
}

Backend is the injected file seam: the raw bytes of the store. Load returns the current contents (empty for a not-yet-written store); Store overwrites them. The host implements this over a real os.File (with O_CREAT and flock); tests use an in-memory backend. PStore only reads on transaction entry and writes at most once, on a committing read-write transaction whose table actually changed.

type Error

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

Error is the error type raised by every PStore operation that fails its transaction-state checks — the analogue of Ruby's PStore::Error. MRI raises it for "not in transaction", "in read-only transaction", "nested transaction", and "undefined key" (the no-default Fetch miss); the messages match.

func (*Error) Error

func (e *Error) Error() string

type Store

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

Store is a PStore over an injected Backend. It holds no open file or lock; the host arranges flock around a transaction. A Store is single-transaction-at-a-time: it tracks whether a transaction is active to reproduce MRI's not-in-transaction and nested-transaction errors.

func New

func New(b Backend) *Store

New returns a Store backed by the given Backend.

func (*Store) Transaction

func (s *Store) Transaction(readOnly bool, body func(t *Tx) error) error

Transaction runs body as a PStore transaction. It loads the table from the backend (an empty backend yields the empty table), runs body, and — on a committing read-write transaction whose table changed — marshals the table back to the backend. body returns t.Commit() or t.Abort() to exit early, any other error to propagate it (no commit happens), or nil to finish normally (which commits a read-write transaction unless Abort was signalled).

readOnly true forbids writes: Set / Delete inside the body raise PStore::Error (Ruby's "in read-only transaction"), and the backend is never written.

Transaction is not reentrant: calling it from within a body raises PStore::Error ("nested transaction"), matching MRI's lock.try_lock guard.

type Tx

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

Tx is the handle passed to a transaction body. Its methods are the in-transaction PStore API; calling them is only legal while the body runs (the engine enforces this). The body returns nil to commit (read-write) or simply finish (read-only), or returns the result of t.Commit() / t.Abort() to exit early.

func (*Tx) Abort

func (t *Tx) Abort() error

Abort signals an early abort and exits the body — PStore#abort. Return its result from the body: no changes are written and the body ends.

func (*Tx) Commit

func (t *Tx) Commit() error

Commit signals an early commit and exits the body — PStore#commit. Return its result from the body: the transaction commits (read-write) and the body ends.

func (*Tx) Delete

func (t *Tx) Delete(key marshal.Value) (marshal.Value, error)

Delete removes key and returns its old value (or nil if absent) — PStore#delete. In a read-only transaction (or outside any transaction) it returns PStore::Error.

func (*Tx) Fetch

func (t *Tx) Fetch(key marshal.Value, def marshal.Value) (marshal.Value, error)

Fetch returns the value for key, or def if key is absent. A nil def reproduces MRI's fetch(key) with no default: a miss raises PStore::Error ("undefined key"). To get a nil default value, pass marshal.Nil{}.

func (*Tx) Get

func (t *Tx) Get(key marshal.Value) (marshal.Value, bool, error)

Get returns the value for key, or (nil, false) if absent — PStore#[] (which returns nil for a missing key). Outside a transaction it returns PStore::Error.

func (*Tx) RootQ

func (t *Tx) RootQ(key marshal.Value) (bool, error)

RootQ reports whether key exists — PStore#root? (alias of PStore#key?). Outside a transaction it returns PStore::Error.

func (*Tx) Roots

func (t *Tx) Roots() ([]marshal.Value, error)

Roots returns the table's keys in insertion order — PStore#roots (alias of PStore#keys). Outside a transaction it returns PStore::Error.

func (*Tx) Set

func (t *Tx) Set(key, val marshal.Value) error

Set creates or replaces the value for key — PStore#[]=. In a read-only transaction (or outside any transaction) it returns PStore::Error.

Jump to

Keyboard shortcuts

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