store

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package store provides universal data access contracts for the Credo framework.

This package defines error sentinels, lifecycle/health interfaces, a connection registry, a registration API, and context-based transaction helpers. It has zero external dependencies — only the Go standard library and Credo's root/fault contracts are imported.

The companion package store/sqldb (a separate Go submodule) wraps *bun.DB with lifecycle management, query builder proxies, error mapping, and transaction support.

Universal Errors

Store errors expose a transport-neutral semantic Kind. The default HTTP policy and future protocol adapters consume that kind independently:

if kind, ok := store.KindOf(err); ok {
    // branch on KindNotFound, KindConstraint, KindDeadlock, ...
}

Error retains diagnostic code/constraint/resource metadata and the original cause without exposing them in Credo's default HTTP response. The sentinel HTTPStatus methods remain only as a deprecated compatibility bridge; new application policy should use Kind.

Registration

Use Register to register a data store connection in the DI container with automatic ping, lifecycle tracking, and health aggregation:

store.Register[*sqldb.DB](app, db)

A value that implements Lifecycle is framework-owned after Register succeeds; the DI container is its sole framework shutdown owner. During one teardown DI makes at most one Shutdown attempt if the still-live shutdown context reaches that registration. A deadline exhausted earlier may leave it uncalled. A value that cannot implement Lifecycle may supply its health handle with WithLifecycle only together with WithCallerOwnedLifecycle; in that explicit mode the caller remains responsible for shutdown. Ownership never transfers on a failed registration.

Registration validates the local DI state and reserves the store name, value type, and resource identity before Ping. By default the Lifecycle value itself is the identity; pointer-backed implementations are recommended. Semantic wrappers around another resource implement LifecycleIdentityProvider and return the underlying pointer or another stable token. Embedding a provider promotes that method through ordinary Go method promotion; there is no reflective field scanning. Within one Registry, the same identity cannot be registered under another type or ownership mode; use credo.App.Alias for interface access to an existing registration. Pending reservations are invisible to Registry readers and readiness; the health entry becomes visible only after Ping and DI publication both succeed. The successful value binding and validated Registry binding are protected against credo.App.Replace so DI cannot diverge from lifecycle/readiness state. A composition-root Registry is adopted with the expected-value compare-and-protect form of credo.App.ProtectBinding: if the resolved pointer changed before protection, registration fails without protecting the replacement. Registry exposes Registry.HealthAll as a read-only view and has no public mutation API.

Identity uniqueness covers only resources registered through Register. Publishing the same Lifecycle again through raw credo.App.Provide, credo.App.ProvideFactory, credo.App.ProvideValue, credo.App.ProvideProtectedValue, or credo.App.Replace is unsupported and can create contradictory ownership or multiple shutdown attempts. In particular, a caller-owned lifecycle handle must not also be registered in DI as a Shutdowner.

Registered stores contribute stable readiness probes. Named and store checks run in parallel with enforced per-check deadlines and panic isolation. Health.Cause carries typed diagnostics for logging while remaining excluded from JSON; free-form Details values are never interpreted as error causes. Overlapping readiness requests share one in-flight execution per store, so a cancellation-ignoring probe cannot accumulate a goroutine per request.

Context-Based Transactions

A custom adapter creates one typed TxScope per logical connection. The transaction type is fixed at construction, preventing a concrete/interface mismatch from silently selecting the fallback connection:

scope := store.NewTxScope[Client]()
ctx = scope.WithTx(ctx, tx)
conn := scope.Conn(ctx, base)
tx, err := scope.RequireTx(ctx) // no fallback

The companion store/sqldb adapter owns a private typed scope per DB. Its proxy terminals participate automatically; db.Conn(ctx) is the transaction-aware Bun escape hatch. The standalone WithTx, GetTx, and Conn helpers remain only as deprecated compatibility APIs.

Index

Constants

View Source
const DefaultPingTimeout = 5 * time.Second

DefaultPingTimeout is the default context deadline for the initial health check performed by Register. Lifecycle.Ping implementations must honor the context; Register does not detach a non-cooperative Ping call.

Variables

View Source
var (
	// ErrNotFound indicates that the requested record does not exist.
	ErrNotFound error = newKindError(KindNotFound, "store: record not found")

	// ErrAlreadyExists indicates a unique or primary-key violation.
	ErrAlreadyExists error = newKindError(KindAlreadyExists, "store: duplicate record")

	// ErrDuplicate is the compatibility name for [ErrAlreadyExists].
	//
	// Deprecated: use ErrAlreadyExists or inspect [KindAlreadyExists].
	ErrDuplicate = ErrAlreadyExists

	// ErrConstraint indicates a persistent integrity constraint violation other
	// than a duplicate, such as foreign-key, not-null, or check failure.
	ErrConstraint error = newKindError(KindConstraint, "store: constraint violation")

	// ErrSerialization indicates that transaction serialization failed.
	ErrSerialization error = newKindError(KindSerialization, "store: serialization failure")

	// ErrDeadlock indicates that the database detected a deadlock.
	ErrDeadlock error = newKindError(KindDeadlock, "store: deadlock")

	// ErrContention indicates a transient lock/busy conflict whose safe retry
	// scope is not implied by this sentinel.
	ErrContention error = newKindError(KindContention, "store: contention")

	// ErrConflict is the legacy umbrella for constraint and concurrency errors.
	// New classifiers return a more specific sentinel whose errors.Is method
	// still matches ErrConflict.
	//
	// Deprecated: inspect ErrConstraint, ErrSerialization, ErrDeadlock, or
	// ErrContention instead.
	ErrConflict error = newKindError(KindConflict, "store: conflict")

	// ErrTimeout indicates that a database operation exceeded a verified
	// deadline or statement timeout.
	ErrTimeout error = newKindError(KindTimeout, "store: timeout")

	// ErrUnavailable indicates that a database connection or service is
	// temporarily unavailable.
	ErrUnavailable error = newKindError(KindUnavailable, "store: unavailable")

	// ErrReadOnly indicates that a write was attempted against a read-only
	// transaction or server.
	ErrReadOnly error = newKindError(KindReadOnly, "store: read-only")
)

Sentinel errors for data access operations.

View Source
var ErrTxMissing = errors.New("store: transaction missing from context")

ErrTxMissing indicates that a transaction-required operation was called without a transaction in its context scope.

Functions

func Conn deprecated

func Conn[T any](ctx context.Context, fallback T) T

Conn returns the transaction from context if present, otherwise returns the fallback connection. Repositories call this in every method for opt-in TX participation.

Deprecated: create a typed TxScope with NewTxScope and use its Conn method. A standalone type-keyed helper cannot isolate multiple logical connections that share T or prevent concrete/interface mismatches.

func ConnInScope

func ConnInScope[T any](ctx context.Context, scope *TxScope[T], fallback T) T

ConnInScope returns the transaction from context for the given scope if present, otherwise returns the fallback connection. Panics if scope is nil.

func GetTx deprecated

func GetTx[T any](ctx context.Context) (T, bool)

GetTx retrieves a transaction handle from the context. Returns the zero value and false if no TX of type T is stored.

Deprecated: create a typed TxScope with NewTxScope and use its GetTx method. A standalone type-keyed helper cannot prevent concrete/interface type mismatches across call sites.

func GetTxInScope

func GetTxInScope[T any](ctx context.Context, scope *TxScope[T]) (T, bool)

GetTxInScope retrieves a scoped transaction handle from the context. Returns the zero value and false if no TX of type T is stored for the scope. Panics if scope is nil.

func IsTransient

func IsTransient(err error) bool

IsTransient reports whether the primary store classification describes a condition that may clear. It never means an operation, transaction callback, or externally visible side effect is safe to retry.

func Register

func Register[R any](app *credo.App, value R, opts ...RegisterOption) error

Register registers value as type R in the DI container, pings the connection, and tracks it in the Registry for lifecycle and health management.

If value implements Lifecycle, it is used directly for Ping, Health, and Shutdown. Otherwise, a separate health handle is accepted only through WithLifecycle plus the explicit WithCallerOwnedLifecycle opt-out.

Steps:

  1. Validate name, lifecycle ownership, and predictable DI conflicts
  2. Resolve or create Registry and wire the internal readiness seam
  3. Privately reserve the unique store name, DI type, and lifecycle identity
  4. Ping the connection with a finite timeout
  5. Publish the DI value and Registry health entry together
  6. Emit validated, secret-free registration warning codes after publication

Shutdown ownership is unambiguous. A direct Lifecycle value is framework-owned after successful registration. The DI container visits it in reverse registration order and makes at most one shutdown attempt if the live shutdown deadline reaches its entry. A separate WithLifecycle handle remains caller-owned and requires WithCallerOwnedLifecycle; the caller must arrange its shutdown. The Registry never closes connections.

On every error, including Ping or final DI publication failure, Register exposes no health entry and does not acquire ownership. This does not undo an independent raw DI publication performed by the caller or another goroutine. Do not place the same lifecycle in DI through Provide, ProvideFactory, ProvideValue, ProvideProtectedValue, or Replace and also through Register; register it once and use credo.App.Alias for additional interface views.

func RequireTxInScope

func RequireTxInScope[T any](ctx context.Context, scope *TxScope[T]) (T, error)

RequireTxInScope retrieves a scoped transaction handle or returns ErrTxMissing when the scope has no transaction in ctx. Panics if scope is nil.

func WithTx deprecated

func WithTx[T any](ctx context.Context, tx T) context.Context

WithTx stores a transaction handle in the context. T is the transaction type (e.g., bun.Tx, bun.IDB). Panics if tx is nil, including a typed-nil pointer stored in an interface T.

Deprecated: create a typed TxScope with NewTxScope and use its WithTx method. A standalone type-keyed helper cannot prevent a producer and consumer from choosing different concrete/interface forms of the same transaction type.

func WithTxInScope

func WithTxInScope[T any](ctx context.Context, scope *TxScope[T], tx T) context.Context

WithTxInScope stores a transaction handle in the context for a specific logical connection scope. Panics if scope is nil — scopes are created once at wiring time via NewTxScope, so a nil scope is a programming error. Panics if tx is nil, including a typed-nil pointer stored in an interface T.

func Wrap

func Wrap(kind error, cause error) error

Wrap preserves cause while classifying it with a store sentinel. Nil causes return nil. Unsupported kinds and an already matching cause are returned unchanged for compatibility.

Types

type Error

type Error struct {
	Kind       Kind
	Op         string
	Resource   string
	Constraint string
	Code       string
	Transient  bool
	Cause      error
}

Error is a structured, transport-neutral data-access error.

Code, Constraint, Resource, and Cause are internal diagnostic metadata. The default HTTP renderer classifies only Kind and never serializes these fields. Transient describes whether the condition may clear; it does not guarantee that replaying the operation or transaction is safe.

func (*Error) Error

func (e *Error) Error() string

Error returns the original cause message when available.

func (*Error) FaultKind

func (e *Error) FaultKind() fault.Kind

FaultKind exposes the semantic category to transport policy packages.

func (*Error) HTTPStatus deprecated

func (e *Error) HTTPStatus() int

HTTPStatus returns the legacy default HTTP mapping for this error.

Deprecated: inspect KindOf and apply transport policy at the application boundary. Credo's root error pipeline now uses FaultKind before this bridge.

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches the exact semantic sentinel and the deprecated ErrConflict umbrella where applicable. Cause matching is handled by Unwrap.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the original driver or application cause.

type Health

type Health struct {
	// Status is the current health state.
	Status HealthStatus

	// Latency is the round-trip time of the health check probe.
	Latency time.Duration

	// Cause is the typed internal reason for a down or degraded result.
	// It is excluded from JSON so unauthenticated readiness endpoints cannot
	// leak connection targets or driver diagnostics. Credo logs it and exposes
	// its text only when HealthConfig.ExposeErrors is explicitly enabled.
	Cause error `json:"-"`

	// Details holds adapter-specific information such as database version,
	// connection pool statistics, or replication lag.
	Details map[string]any
}

Health is the result of a connection health check.

func (Health) Clone

func (h Health) Clone() Health

Clone returns a defensive copy of the health snapshot. Cause identity is preserved. Details is cloned at the top level; nested maps, slices, pointers, and other reference values remain shared.

type HealthStatus

type HealthStatus string

HealthStatus represents the health state of a data store connection.

const (
	// StatusUp indicates the connection is healthy.
	StatusUp HealthStatus = "UP"

	// StatusDown indicates the connection is unreachable.
	StatusDown HealthStatus = "DOWN"

	// StatusDegraded indicates the connection is available but impaired.
	StatusDegraded HealthStatus = "DEGRADED"
)

type Kind

type Kind = fault.Kind

Kind is the transport-neutral semantic category of a store error.

const (
	KindUnknown       Kind = fault.KindUnknown
	KindNotFound      Kind = fault.KindNotFound
	KindAlreadyExists Kind = fault.KindAlreadyExists
	KindConstraint    Kind = fault.KindConstraint
	KindSerialization Kind = fault.KindSerialization
	KindDeadlock      Kind = fault.KindDeadlock
	KindContention    Kind = fault.KindContention
	KindConflict      Kind = fault.KindConflict
	KindTimeout       Kind = fault.KindTimeout
	KindUnavailable   Kind = fault.KindUnavailable
	KindReadOnly      Kind = fault.KindReadOnly
)

Store error kinds. They are aliases of the shared transport-neutral kinds so Credo's HTTP policy and future gRPC policy can classify them without importing store and creating a package cycle.

func KindOf

func KindOf(err error) (Kind, bool)

KindOf finds the first recognized store Kind in err's unwrap/join tree.

type Lifecycle

type Lifecycle interface {
	// Ping verifies the connection is alive and must honor ctx cancellation;
	// Register invokes it synchronously with a deadline.
	Ping(ctx context.Context) error

	// Shutdown gracefully closes the connection.
	// Implementations should respect ctx.Done() for timely cleanup.
	Shutdown(ctx context.Context) error

	// Health returns structured health information including status,
	// latency, and adapter-specific details (pool stats, version, etc.).
	Health(ctx context.Context) Health
}

Lifecycle manages connection health and shutdown for a data store. Adapters (e.g., store/sqldb) implement this interface for use with Register. Implementations should normally be pointer-backed so Register can retain a stable physical-resource identity and reject duplicate ownership.

type LifecycleIdentityProvider

type LifecycleIdentityProvider interface {
	Lifecycle
	ResourceIdentity() any
}

LifecycleIdentityProvider is an optional Lifecycle extension for semantic wrappers that represent another physical resource. ResourceIdentity must return a non-nil, comparable, reflexively equal token that remains stable for the resource lifetime; the underlying resource pointer is the usual token. Wrapper types that embed an implementation inherit this method automatically.

type RegisterOption

type RegisterOption func(*registerOptions)

RegisterOption configures a Register call.

func WithCallerOwnedLifecycle

func WithCallerOwnedLifecycle() RegisterOption

WithCallerOwnedLifecycle explicitly keeps lifecycle shutdown ownership with the caller when WithLifecycle is used for a value that cannot itself implement Lifecycle. The caller must arrange shutdown, for example through credo.App.OnShutdown. The option is invalid without WithLifecycle.

func WithLifecycle

func WithLifecycle(lc Lifecycle) RegisterOption

WithLifecycle provides the handle used for Ping and Health when value does not implement Lifecycle itself. It must be paired with WithCallerOwnedLifecycle: the caller retains responsibility for invoking Shutdown on that separate handle. Prefer making value implement Lifecycle so the framework can own shutdown.

func WithName

func WithName(name string) RegisterOption

WithName sets the stable identifier used in health reporting. It rejects an empty or padded name, control characters, and the reserved "credo." prefix. If omitted, Register uses the pointer-unwrapped, package-qualified name of R.

func WithPingTimeout

func WithPingTimeout(d time.Duration) RegisterOption

WithPingTimeout overrides the default Ping context deadline (5s) for the initial health check performed by Register.

type Registry

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

Registry tracks data store connections for startup ping and health aggregation. It is created automatically on the first Register call and stored in the DI container.

Registry is a read-only public facade; Register is its only mutation path. It never closes connections. The DI container owns direct Lifecycle values and makes at most one shutdown attempt per value if the live shutdown deadline reaches its entry. Explicitly caller-owned lifecycle handles remain the caller's responsibility.

func (*Registry) HealthAll

func (r *Registry) HealthAll(ctx context.Context) map[string]Health

HealthAll returns health status for all tracked connections. The returned map is keyed by the registration name.

type TxScope

type TxScope[T any] struct {
	// contains filtered or unexported fields
}

TxScope isolates transactions that share the same Go type but belong to different logical connections. T is fixed when the scope is created, so a concrete transaction cannot be stored and then silently missed by reading the same scope through a different interface type.

The marker field keeps TxScope non-zero-size on purpose: in Go, pointers to distinct zero-size structs may share an address, which would let two independent scopes collide as context keys. A non-empty field guarantees a unique address per NewTxScope call.

func NewTxScope

func NewTxScope[T any]() *TxScope[T]

NewTxScope creates a unique transaction scope whose transaction type is T.

func (*TxScope[T]) Conn

func (s *TxScope[T]) Conn(ctx context.Context, fallback T) T

Conn returns this scope's transaction from the context if present, otherwise the fallback connection — the call repositories make for opt-in scoped TX participation. It is the method form of ConnInScope.

func (*TxScope[T]) GetTx

func (s *TxScope[T]) GetTx(ctx context.Context) (T, bool)

GetTx retrieves this scope's transaction handle from the context, returning the zero value and false when none is stored. It is the method form of GetTxInScope.

func (*TxScope[T]) RequireTx

func (s *TxScope[T]) RequireTx(ctx context.Context) (T, error)

RequireTx retrieves this scope's transaction handle or returns ErrTxMissing when none is stored. It never falls back to a base connection. It is the method form of RequireTxInScope.

func (*TxScope[T]) WithTx

func (s *TxScope[T]) WithTx(ctx context.Context, tx T) context.Context

WithTx stores a transaction handle in the context for this scope. It is the method form of WithTxInScope. It panics for nil transaction values, including typed-nil pointers held by an interface T.

Directories

Path Synopsis
sqldb module

Jump to

Keyboard shortcuts

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