This file provides guidance for AI assistants working on this codebase.
Append-only log library for Go with typed generic entries, schema versioning, deduplication, and pluggable storage backends (SQLite, PostgreSQL, MongoDB).
Store[I comparable](store.go): Backend interface, generic over ID type (int64 for SQL, string for MongoDB). Handles raw bytes.Stream[I comparable, T any](stream.go): Lightweight typed handle. Encodes/decodes payloads via Codec, applies schema upcasting on read.
ledger/
├── store.go # Store interface, RawEntry, StoredEntry, ReadOptions, HealthChecker
├── stream.go # Stream[I,T], Entry, AppendInput, options
├── codec.go # Codec interface, JSONCodec
├── schema.go # Upcaster interface, FieldMapper, UpcasterFunc, upcastChain
├── errors.go # Sentinel errors
├── validate.go # ValidateName for table/collection names
├── storetest/ # Backend-agnostic conformance test suite
│ └── storetest.go # RunStoreTests[I](t, store, afterFn)
├── sqlite/ # SQLite backend (Store[int64])
│ └── store.go
├── postgres/ # PostgreSQL backend (Store[int64])
│ └── store.go
└── mongodb/ # MongoDB backend (Store[string])
└── store.go
- Functional options:
New(ctx, db, ...Option)with unexportedoptionsstructs - Compile-time checks:
var _ ledger.Store[int64] = (*Store)(nil) - Lightweight streams: Value type (not pointer), create per operation, discard after use
- Schema versioning:
SchemaVersionstored per-entry as column/field, upcasters applied on read - Dedup: Partial unique index on
(stream, dedup_key) WHERE dedup_key != '' - Cursor-based pagination:
After[I](id)+Limit(n)(default 100) +Desc() - Metadata:
map[string]stringon all entry types, stored as JSON (SQL) or BSON subdocument (MongoDB)
WithCodec(c Codec)— custom payload encoder/decoder (default: JSONCodec)WithSchemaVersion(v int)— version stamped on new entries (default: 1)WithUpcaster(u Upcaster)— register version migration (v1→v2, v2→v3, etc.)
After[I](id)— cursor-based paginationLimit(n)— max entries (default 100)Desc()— newest firstWithOrderKey(key)— filter by ordering key
type Store[I comparable] interface {
Append(ctx, stream, ...RawEntry) ([]I, error)
Read(ctx, stream, ...ReadOption) ([]StoredEntry[I], error)
Count(ctx, stream) (int64, error)
Trim(ctx, stream, beforeID) (int64, error)
Close(ctx) error
}HealthChecker:Health(ctx) error— all backends implement this
SQL backends (sqlite, postgres) use transactions for atomic batch inserts. MongoDB uses InsertMany with ordered:false — partial success is possible on non-dedup errors.
go build ./... # Build
go test ./... # Unit tests (SQLite only)
just test-integration # All backends with Docker
just test-sqlite # SQLite only
just test-pg # PostgreSQL only
just test-mongo # MongoDB only
just bench # Benchmarks
just lint # golangci-lint# Environment variables for manual testing
POSTGRES_DSN=postgres://ledger_test:ledger_test@localhost:5434/ledger_test?sslmode=disable
MONGO_URI=mongodb://localhost:27020/?directConnection=true- Sentinel errors checked with
errors.Is():ErrStoreClosed,ErrEncode,ErrDecode,ErrNoUpcaster,ErrInvalidCursor,ErrInvalidName - Backend errors wrapped with context:
fmt.Errorf("ledger/sqlite: ...: %w", err)
ctx context.Contextas first parameter- Return value types, not pointers to value types
- Unexport internal types; only expose Option functions and interfaces
- No panics except in initialization (
NewStreampanics on nil store)