nestory

package module
v0.0.0-...-9e223c1 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: GPL-2.0 Imports: 20 Imported by: 0

README

nestory

Nestory is a persistent in-memory object graph for Go. Your structs are the database: the complete dataset lives in RAM, relations are ordinary *T pointers, and committed changes are recovered from a write-ahead log.

[!WARNING] Nestory is alpha software. Its API and on-disk format may change. Use it for experiments and replaceable data, not as the only copy of production data.

type Profile struct {
	Id    int
	Theme string
}

func (profile Profile) GetId() int { return profile.Id }

type User struct {
	Id      int
	Name    string
	Profile *Profile `rel:"own,Id"`
}

func (user User) GetId() int { return user.Id }

nestory.DataDir = "./data"

if err := nestory.Register[Profile](); err != nil {
	log.Fatal(err)
}
if err := nestory.Register[User](); err != nil {
	log.Fatal(err)
}

nestory.Open[Profile]()
users := nestory.Open[User]()

user := &User{Name: "Ada", Profile: &Profile{Theme: "dark"}}
if err := users.Create(user); err != nil {
	log.Fatal(err)
}

err := users.View(user.Id, func(user *User) error {
	fmt.Println(user.Profile.Theme) // already wired; no query or join
	return nil
})

Why Nestory?

  • Normal Go structs and pointers, without generated models, proxies, or setters.
  • Stable pointers in a chunked arena; inserts do not move existing objects.
  • Detached branches and optimistic transactions for safe edits.
  • Zero-copy callback reads with View, ViewMany, ViewRange, and indexed delta cursors.
  • Durable unique and ordered composite indexes.
  • A value-lifetime model that derives cascade delete, borrow vetoes, optional references, and computed inverse views from explicit ownership roles.
  • WAL-backed commits, including one crash-atomic frame for transactions that touch multiple entity types.
  • An explicitly unsafe live-pointer API for exclusive, allocation-sensitive workloads.

Nestory deliberately keeps the whole dataset in memory. It is a good fit for small, hot object graphs and embedded services; it is not a replacement for a large disk-first database.

Install

go get github.com/DorianDevp/nestory

Nestory currently requires Go 1.24.

Documentation

Status

The core read, transactional, relation, indexing, and recovery paths are race-tested, but the project is still an alpha. Important current limitations include an in-memory working set, integer primary keys, process-global registration, no schema migration system, and no stable on-disk compatibility promise. See How it works for the full list.

License

Nestory is licensed under the GNU General Public License v2.0. See LICENSE.

Documentation

Overview

Package nestory persists a Go object graph to a folder of .gob files and rehydrates the whole pointer graph on load. No queries, no joins, relations declared via struct tags become real Go pointers the moment you call Open.

Tags:

key:"primary"          int Id field (required).
key:"unique"           unique scalar secondary index.
index:"name,1,unique"  ordered/composite index field and position.
rel:"own,Id"           required owning pointer.
rel:"borrow,Id"        required non-owning pointer with delete veto.
rel:"option,Id"        nullable non-owning pointer.
rel:"inverse,User"     computed reverse collection.

Alpha. The API will change. Don't store anything you can't lose.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrRelationSchema    = errors.New("nestory: invalid relation schema")
	ErrRelationInvariant = errors.New("nestory: relation invariant violated")
	ErrDeleteRestricted  = errors.New("nestory: delete restricted by relation")
)
View Source
var AuditTrackedWrites bool

AuditTrackedWrites makes every tracked write also run the full branch diff and fail when a node changed that Edit never named. It costs exactly what declaring nothing would have cost, so switch it on in tests, that is where a forgotten Edit should surface, rather than as a write that quietly vanishes.

View Source
var DataDir = "./data"

DataDir is where every base writes its files. Override before Register.

View Source
var ErrAlreadyExists = errors.New("nestory: entity already exists")
View Source
var ErrConflict = errors.New("nestory: transaction conflict")

ErrConflict: someone committed one of our resources after we snapshotted. The snapshot is refreshed in place so a retry runs against fresh data.

View Source
var ErrExpiredSnapshot = errors.New("nestory: expired snapshot")

ErrExpiredSnapshot: the snapshot's contract is gone (committed, discarded, or never came from Get).

View Source
var ErrNotFound = errors.New("nestory: entity not found")
View Source
var ErrUndeclaredWrite = errors.New("nestory: node changed without a matching Edit")

ErrUndeclaredWrite reports a node that changed without a matching Edit.

View Source
var ErrUniqueViolation = errors.New("nestory: unique index violation")

Functions

func Edit

func Edit[C Entity](writes *Writes, node *C) *C

Edit declares that node is about to change and returns it unchanged, so the declaration is what produces the value written through:

child := nestory.Edit(w, owner.Children[3])
child.Value = 11

Writing straight through owner.Children[3] instead compiles and runs, but the change is not published. Set AuditTrackedWrites in tests to turn that into a failure.

func Register

func Register[T Entity]() error

Register loads T's chunk files and inflates each row into a *T. Call once per type, before any Open: fillRelation needs every type registered to wire pointers.

Types

type DB

type DB[T Entity] struct {
	// contains filtered or unexported fields
}

DB is the in-memory state for one entity type. Construct via Open.

func Open

func Open[T Entity]() *DB[T]

Open returns a typed DB over the entities loaded by Register. Call after every type used by rel has been registered.

func (*DB[T]) Create

func (db *DB[T]) Create(entity *T) error

Create persists entity and its new ownership subtree in one short transaction. Use Transaction to batch it with more work.

func (*DB[T]) Delete

func (db *DB[T]) Delete(id int) error

Delete removes id and its complete owned subtree in one short transaction. Use Transaction and Join to combine it with operations on other entity types.

func (*DB[T]) DeleteByIndex

func (db *DB[T]) DeleteByIndex(indexName string, prefix []any) error

DeleteByIndex removes every row matching an ordered index prefix. The prefix is resolved once; rows appended after that snapshot belong to a later state.

func (*DB[T]) DeleteMany

func (db *DB[T]) DeleteMany(ids []int) error

DeleteMany removes ids in one transaction and one durable WAL frame.

func (*DB[T]) Filter

func (db *DB[T]) Filter(filterFn func(T) bool) []T

Filter returns detached root values matching filterFn. Use Transaction for writable ownership branches.

func (*DB[T]) FindOneBy

func (db *DB[T]) FindOneBy(field string, value any) (*T, error)

FindOneBy returns a detached branch for the first matching entity.

func (*DB[T]) Get

func (db *DB[T]) Get(id int) (*T, error)

Get returns a detached branch containing id and its complete ownership subtree. Mutate owned nodes and hand the root to Update to merge the branch.

func (*DB[T]) Join

func (db *DB[T]) Join(context TransactionContext) *Tx[T]

Join binds db to an existing transaction without opening a nested commit.

func (*DB[T]) Len

func (db *DB[T]) Len() int

Len returns the number of live entities.

func (*DB[T]) Tracked

func (db *DB[T]) Tracked() TrackedDB[T]

Tracked enters the write-declaring API without allocating.

func (*DB[T]) Transaction

func (db *DB[T]) Transaction(fn func(*Tx[T]) error) error

Transaction runs fn against one shared transaction context and commits every changed branch once. Returning an error discards the complete working set.

func (*DB[T]) TypeName

func (db *DB[T]) TypeName() string

TypeName returns the Go type name of T without package prefix.

func (*DB[T]) Unsafe

func (db *DB[T]) Unsafe() UnsafeDB[T]

Unsafe enters the live-pointer API without allocating.

func (*DB[T]) Update

func (db *DB[T]) Update(branch *T) error

Update merges the detached ownership branch returned by Get.

func (*DB[T]) UpdateWithin

func (db *DB[T]) UpdateWithin(id int, fn func(*T) error) error

UpdateWithin retries a short transaction on conflict. fn may run more than once and must therefore express intent without external side effects.

func (*DB[T]) View

func (db *DB[T]) View(id int, fn func(*T) error) error

View runs fn against the stable live pointer for id without cloning its ownership tree. The tree is read-locked for the callback. References outside that tree are navigable but are not covered by the same consistency window. Mutating or retaining the pointer for synchronized use after fn returns violates the View contract; use Transaction or Unsafe for writes.

func (*DB[T]) ViewMany

func (db *DB[T]) ViewMany(ids []int, fn func([]*T) error) error

ViewMany exposes stable live pointers for ids during fn without cloning. IDs are locked in ascending order. The pointers are read-only by contract and must not be retained for synchronized use after fn returns.

func (*DB[T]) ViewRange

func (db *DB[T]) ViewRange(indexName string, prefix []any, fn func([]*T) error) error

ViewRange visits an ordered index prefix. For an index on (SessionID, Seq), prefix []any{sessionID} returns one session in Seq order. Returned pointers obey the same callback-only read contract as ViewMany.

func (*DB[T]) ViewRangeAfter

func (db *DB[T]) ViewRangeAfter(
	indexName string,
	prefix []any,
	after any,
	fn func([]*T) error,
) error

ViewRangeAfter visits the part of an ordered index prefix whose next index field is strictly greater than after. For an index on (SessionID, Seq), a prefix containing SessionID and an after Seq form an efficient delta cursor.

type Entity

type Entity interface {
	GetId() int
}

Entity is the contract every persisted struct must satisfy. Id is the primary key; auto-assigned on Create if left zero.

type Tower

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

Tower coordinates one persistent shadow graph for the complete project. Tables remain separate inside the replica, while relation pointers are wired exclusively to nodes from the same shadow world.

type TrackedDB

type TrackedDB[T Entity] struct {
	// contains filtered or unexported fields
}

TrackedDB is the write-declaring API. An owned child is compared against committed state only when Edit hands it over, so a callback that touches a handful of nodes costs a handful of comparisons instead of one per node in the branch. Reading stays ordinary pointer traversal.

The root is always compared, so a callback that only changes the root needs no Edit at all.

func (TrackedDB[T]) UpdateWithin

func (tracked TrackedDB[T]) UpdateWithin(id int, fn func(*Writes, *T) error) error

UpdateWithin retries a short transaction on conflict, like DB.UpdateWithin, and additionally limits the comparison to the root plus whatever fn declares through Edit. fn may run more than once and must therefore express intent without external side effects.

Roots outside the relation graph, and roots that own nothing, have no branch to skip: those fall back to the ordinary path and the declaration is unused.

type TransactionContext

type TransactionContext interface {
	// contains filtered or unexported methods
}

TransactionContext is implemented by a live Tx. Join uses it to expose a differently typed DB inside the same commit context.

type Tx

type Tx[T Entity] struct {
	// contains filtered or unexported fields
}

Tx is a transaction-local view of one root entity type. Objects returned by Get are detached branches; all mutations are detected and committed when the Transaction callback returns nil.

func (*Tx[T]) Create

func (tx *Tx[T]) Create(entity *T) error

Create stages entity and its new ownership subtree in this transaction.

func (*Tx[T]) Delete

func (tx *Tx[T]) Delete(id int) error

Delete stages id and its committed ownership subtree for deletion.

func (*Tx[T]) DeleteMany

func (tx *Tx[T]) DeleteMany(ids []int) error

DeleteMany stages ids in the same transaction. Duplicate IDs are harmless; either every surviving row is deleted at commit or none is.

func (*Tx[T]) Get

func (tx *Tx[T]) Get(id int) (*T, error)

Get loads id and its ownership subtree into this transaction. Repeated loads of the same node return the same transaction-local pointer.

func (*Tx[T]) UpdateWithin

func (tx *Tx[T]) UpdateWithin(id int, fn func(*T) error) error

UpdateWithin mutates id inside this transaction. It joins the current context and does not commit independently.

type UnsafeDB

type UnsafeDB[T Entity] struct {
	// contains filtered or unexported fields
}

UnsafeDB exposes stable live pointers. The caller must provide exclusive access until Flush completes; validation errors do not roll mutations back.

func (UnsafeDB[T]) All

func (unsafe UnsafeDB[T]) All() []*T

All returns every stable live pointer and marks every chunk dirty.

func (UnsafeDB[T]) Create

func (unsafe UnsafeDB[T]) Create(entity *T)

Create stages an entity for the next Flush without transaction isolation.

func (UnsafeDB[T]) Delete

func (unsafe UnsafeDB[T]) Delete(id int) error

Delete stages a live delete for the next Flush.

func (UnsafeDB[T]) Flush

func (unsafe UnsafeDB[T]) Flush() error

Flush validates the complete live graph, applies unsafe deletes, and writes every dirty chunk.

func (UnsafeDB[T]) Get

func (unsafe UnsafeDB[T]) Get(id int) (*T, error)

Get returns the stable live pointer for id and marks its chunk dirty.

type Writes

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

Writes collects the nodes a tracked callback promises to change.

Jump to

Keyboard shortcuts

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