Help us shape the future of Go! Take the 2026 Go Developer Survey

priomptdb

package module
v0.7.2 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

priompt db-adapters

The filing cabinet of Priompt. Everything a Priompt server knows — every prompt, every historical version, every branch pointer — is physically kept by this library, in a real database, with optional encryption so a stolen database file is useless.

It ships two backends and is deliberately structured so adding a third is a one-file job:

Backend When to use it
SQLite (the default) Self-hosting, single server. Pure-Go driver — no installs, the database is just a file
PostgreSQL Multiple server nodes sharing one database

What's inside the cabinet

flowchart TD
    R["refs<br/>branch name → newest commit<br/>(main, feature, …)"] -->|points into| C["commits<br/>every version ever published,<br/>who wrote it, when, and why"]
    C -->|"newest main version is copied into"| P["prompts<br/>what agents are served right now<br/>(the 'served HEAD')"]

Three tables — that's the whole story:

  • prompts — the current live version of each prompt (what agents get).
  • commits — the full history: every version, content-addressed, with parent links (like Git commits), author, message, timestamp.
  • refs — branch pointers: which commit is the tip of main, feature, …

A commit on main updates all three in one transaction, so the live version can never drift from the recorded history. Schema changes are ordered, append-only migration steps, applied automatically and idempotently at Open.

Using it

import store "github.com/priompt/db-adapters"

st, _ := store.Open("prompts.db")                  // a file path -> SQLite
st, _ := store.Open("postgres://user:pw@host/db")  // a postgres:// DSN -> PostgreSQL

The API covers the whole lifecycle: Put/Get/List/Dump for the served HEAD, Commit/Branch/Merge/Log/Resolve/SetBranch/GetCommit for history, branching, and rollback.

Encryption at rest

Set one environment variable and prompt content is encrypted with AES-256-GCM before it touches the disk:

export PRIOMPT_ENCRYPTION_KEY=$(head -c32 /dev/urandom | base64)   # base64 of 32 bytes

Think of it as the cabinet's lock: anyone who steals the drawer (the .db file, a Postgres dump) gets ciphertext. The honest fine print:

  • At rest, not end-to-end — the server holds the key and decrypts to do its job (validation, semantic diff).
  • No migration to turn it on — encrypted values carry a marker, so old plaintext rows stay readable while new writes are encrypted.
  • The version_hash column stays plaintext (it's a fingerprint, not content), so caching and deduplication keep working.
  • Lose the key and encrypted rows are unrecoverable. Every process that opens the same database must share the key.

How the pluggable-backend part works

All SQL in this library is portable and written once, in ?-placeholder style. A backend ("dialect") only tells the library three things:

flowchart TD
    API["Store API<br/>Put / Get / Commit / Merge / Log …<br/>(portable SQL, written once)"] --> D{"dialect"}
    D --> S["sqlite.go<br/>driver: sqlite<br/>DSN: any file path<br/>placeholders: ? as-is"]
    D --> P["postgres.go<br/>driver: pgx<br/>DSN: postgres://…<br/>placeholders: rewrite ? → $1,$2"]
    D --> F["your-db.go<br/>(future — one file)"]
type dialect struct {
    driver string                // database/sql driver name
    match  func(dsn string) bool // does this DSN belong to this backend?
    rebind func(q string) string // ?-style placeholders -> native style
}
Adding a database
  1. Create <name>.go with a dialect var, blank-importing its driver — postgres.go is the pattern to copy.
  2. Add it to the dialects list in dialect.go, before sqlite (sqlite is the catch-all: any DSN nobody claims is treated as a file path).
  3. Check upserts: the queries use ON CONFLICT, which SQLite and Postgres share. An engine without it (e.g. MySQL's ON DUPLICATE KEY) needs a per-dialect upsert clause — add that to dialect when such a backend actually lands.

Tests run on SQLite in a temp directory — go test ./..., nothing to install.

Relation to the core repo

The Priompt server imports this module directly (as github.com/priompt/db-adapters, a tagged version) — there is one copy of the storage engine and this repo is it. To change it alongside the server, use a go.work in the parent directory (go work init ./db-adapters ./priompt).

Documentation

Overview

Package priomptdb is the durable, ACID prompt store for Priompt. Backends are per-file dialects (sqlite.go, postgres.go — see dialect.go to add one), chosen from the DSN at Open. The schema and every query are portable, written once in ?-placeholder style; a dialect supplies the driver and the placeholder rewrite.

Index

Constants

View Source
const DefaultBranch = "main"

DefaultBranch is the trunk a prompt's history accrues on unless told otherwise.

Variables

View Source
var ErrAlreadyExists = errors.New("branch already exists")

ErrAlreadyExists means the named branch is already present on this prompt.

View Source
var ErrBranchNotFound = errors.New("branch not found")
View Source
var ErrConflict = errors.New("branch moved: concurrent update")

ErrConflict means the branch moved between reading its tip and advancing it — someone else committed first. It is retryable: re-read the tip and reapply.

View Source
var ErrNotFound = errors.New("prompt not found")

Functions

func CommitHash

func CommitHash(uri, versionHash, parent, parent2, author, message string) string

CommitHash is the commit identity: which prompt, plus content, plus lineage, plus authorship. Two commits with the same content but different parents are distinct nodes, which is what lets history branch and reverts not collide with the original.

The uri is part of the identity, and must be. Git gets this for free because a commit names a tree and a tree encodes path -> blob: identical file content shares a *blob* (safe, and deliberate dedup) while the commit still differs by path. This store snapshots content inline with no tree layer, so leaving the uri out collapsed the two: any two prompts with the same template, author and message produced the same hash, the second INSERT hit ON CONFLICT DO NOTHING and was dropped, and the surviving row kept the *first* prompt's uri. The second prompt's branch then pointed at a commit that disowned it, which corrupted its history (across org boundaries) and made rollback impossible.

func Hash

func Hash(template string, slots []string) string

Hash is the version identity: same content -> same hash -> same cache key.

Types

type Commit

type Commit struct {
	Hash        string
	URI         string
	Template    string
	Slots       []string
	VersionHash string
	Parent      string // "" for the root commit
	Parent2     string // set only on merge commits
	Author      string
	Message     string
	CreatedAt   time.Time
}

Commit is one node in a prompt's history DAG.

type Prompt

type Prompt struct {
	URI         string
	Template    string
	Slots       []string
	VersionHash string
}

type Ref

type Ref struct {
	URI    string
	Branch string
	Commit string
}

Ref is one branch pointer: which commit a branch of a prompt points at.

type Sealer

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

A Sealer encrypts and decrypts values with the configured key. It is exported because the database is not the only place prompt content comes to rest: the server's shared Redis cache holds the same templates, and an "encrypted at rest" guarantee that stops at one of two persistent stores is not a guarantee. One implementation of the crypto, used by both.

func NewSealer

func NewSealer() (*Sealer, error)

NewSealer builds a Sealer from PRIOMPT_ENCRYPTION_KEY. A nil-aead Sealer (no key configured) is valid and passes values through unchanged, so callers need no branch of their own.

func (*Sealer) Enabled

func (s *Sealer) Enabled() bool

Enabled reports whether a key is configured.

func (*Sealer) OpenBytes

func (s *Sealer) OpenBytes(stored []byte) ([]byte, error)

OpenBytes reverses SealBytes. A value without the marker is returned as-is.

func (*Sealer) Seal

func (s *Sealer) Seal(plain string) string

Seal encrypts a value; passthrough when no key is configured.

func (*Sealer) SealBytes

func (s *Sealer) SealBytes(plain []byte) []byte

SealBytes is Seal for opaque binary payloads — a marshalled protobuf, say.

type Store

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

func Open

func Open(dsn string) (*Store, error)

Open connects to SQLite (a file path) or PostgreSQL (a postgres:// DSN) and applies any pending schema migrations.

func (*Store) Branch

func (s *Store) Branch(ctx context.Context, uri, name, from string) (string, error)

Branch creates a new branch pointing at the current tip of from. The new branch shares from's history until they diverge. Returns that tip's commit hash — the caller already needs it and Branch has already resolved it internally, so there's no reason to make them look it up again.

func (*Store) Branches added in v0.7.2

func (s *Store) Branches(ctx context.Context, uri string) ([]Ref, error)

Branches returns a prompt's branch pointers, sorted by name.

func (*Store) Close

func (s *Store) Close() error

func (*Store) Commit

func (s *Store) Commit(ctx context.Context, uri, branch, template string, slots []string, author, message string) (string, error)

Commit appends a new version onto branch and advances the branch pointer to it. The branch is created (rooted at this commit) if it does not yet exist. Author and message are the collaboration metadata: who changed what, and why. Returns the new commit hash; committing identical content+meta onto the same tip is idempotent (same hash, pointer unchanged).

func (*Store) Commits added in v0.7.2

func (s *Store) Commits(ctx context.Context, uri string) ([]Commit, error)

Commits returns every commit of a prompt, newest first — on any branch, a merged-away side branch, or none at all (a commit left behind by a rollback). Log only follows one branch's first parents, so it can't reach those.

func (*Store) CountCommits

func (s *Store) CountCommits(ctx context.Context) (int, error)

CountCommits reports how many commits the store holds. Restore uses it to refuse to write over a database that already has history.

func (*Store) Dump

func (s *Store) Dump(ctx context.Context) ([]Prompt, error)

Dump returns every stored prompt, ordered by URI — the backup snapshot.

func (*Store) DumpCommits

func (s *Store) DumpCommits(ctx context.Context) ([]Commit, error)

DumpCommits returns every commit in the store, oldest first so that a restore can insert them in an order where each commit's parents already exist.

func (*Store) DumpRefs

func (s *Store) DumpRefs(ctx context.Context) ([]Ref, error)

DumpRefs returns every branch pointer in the store.

func (*Store) Get

func (s *Store) Get(ctx context.Context, uri string) (Prompt, error)

func (*Store) GetCommit

func (s *Store) GetCommit(ctx context.Context, hash string) (Commit, error)

GetCommit fetches a single commit by hash.

func (*Store) List

func (s *Store) List(ctx context.Context, prefix string) ([]Prompt, error)

List returns the served-HEAD prompts whose URI starts with prefix, sorted by URI — the repo/tree browse. An empty prefix lists the whole store. Only uri and version_hash are populated (a listing, not a fetch).

func (*Store) Log

func (s *Store) Log(ctx context.Context, uri, branch string) ([]Commit, error)

Log walks the first-parent chain from a branch tip back to the root — the mainline history, newest first. (Like `git log --first-parent`; second parents are reachable via GetCommit when you need the full DAG.) Done as one recursive query rather than one round-trip per commit: a branch's history only grows, and an N-round-trip walk gets slower every time something is published to it.

func (*Store) Merge

func (s *Store) Merge(ctx context.Context, uri, into, from, author, message string) (string, error)

Merge records a merge commit on into, with into's tip as first parent and from's tip as second parent, and advances into. Returns the merge commit hash.

ponytail: resolution is "take theirs" — the merge content is from's tip verbatim. That records the graph correctly and lets a semantic diff against the first parent show exactly what the merge pulled in. Add 3-way content auto-merge when divergent edits to the *same* prompt actually need reconciling.

func (*Store) Put

func (s *Store) Put(ctx context.Context, p Prompt) error

func (*Store) PutCommit

func (s *Store) PutCommit(ctx context.Context, c Commit) error

PutCommit inserts a commit verbatim, preserving its hash, parents, author, message and timestamp. Restoring must not recompute the hash: the hash is the commit's identity, every child names it as a parent, and recomputing would break the chain. Re-inserting an existing commit is a no-op.

func (*Store) PutRef

func (s *Store) PutRef(ctx context.Context, r Ref) error

PutRef points a branch at a commit, creating or moving it. Unlike SetBranch this does not re-materialize the served HEAD or check that the commit belongs to the prompt — a restore replays recorded state rather than performing a rollback, and the prompts table is restored from its own records.

func (*Store) Resolve

func (s *Store) Resolve(ctx context.Context, uri, ref string) (Commit, error)

Resolve maps a ref to its commit. A ref is a branch name (resolved to its tip) or a commit hash. Returns ErrNotFound if it is neither, or names a commit that belongs to a different prompt.

func (*Store) SchemaVersion

func (s *Store) SchemaVersion() int

SchemaVersion is the number of migrations applied to this database.

func (*Store) SetBranch

func (s *Store) SetBranch(ctx context.Context, uri, branch, commitHash string) (Commit, error)

SetBranch points branch at an existing commit — rollback (move main back) or pinning a branch. On main it re-materializes the served HEAD in the same transaction. The commit must belong to uri. Returns the now-tip commit.

Jump to

Keyboard shortcuts

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