Documentation
¶
Overview ¶
Package sqlite is the thread's second durable backend: every session in one SQLite file on the CGO-free modernc.org/sqlite driver — the store's choice (store/sqlite, Crush's before it), reused so one dependency serves both modules — with WAL and embedded migrations. It is its own module because the driver would otherwise leak into thread's go.mod (ADR 0011 §1: "own module only if its driver would leak into thread" — it would; thread stays root-and-stdlib only).
A session's bytes are the same lines jsonl writes — the header line, then one entry line per row, the thread wire verbatim (ADR 0011 §2, §6) — so the backends differ in where the lines live, never in what they say: Load here decodes exactly what Load there would, the threadtest table runs the same on both, and the loud rules (the unknown kind and the newer version are ErrNewerFormat, never a skip; anything else undecodable is ErrCorrupt naming the line; salvage or not) are thread's own decode path, not a reimplementation.
The one-writer rule (ADR 0011 §5) is a lock row per session, taken on a session's first write and held until Delete or process exit: a second writer — another Storage in this process, or a process on this machine — fails with thread.ErrLocked. A holder that has died (checked by pid, on the holder's own host) is taken over, so a crashed writer never strands its session: flock's death-release semantics, rebuilt on the database the backend already needs. Readers never lock — Load and List always work.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNewerSchema = errors.New("sqlite: database schema is newer than this weft")
ErrNewerSchema is returned by Open when the file's schema_migrations is ahead of this binary's highest migration: a database written by a newer weft fails loudly instead of running nothing and saying nothing — the store's rule (its ErrNewerSchema), verbatim, because the failure is the same failure.
Functions ¶
func Open ¶
Open opens (creating if needed) the sessions database at path and brings its schema up to date, returning a thread.Storage. ":memory:" works — one in-process database for tests and examples, a shape the store's Open shares. Connections carry synchronous=NORMAL, busy_timeout=30000, foreign_keys=ON, and immediate write transactions (preventing deferred-to-writer upgrade deadlocks), and the handle allows a single connection so every write is serialized. WAL is not a connection pragma: it is a persistent property of the file, switched once by the first Open (setWAL), so a reader in another process — a watcher, the Inspector — reads concurrently through its own Open.
The shared open vocabulary (thread.ResolveOpen) applies: Salvage downgrades a malformed line from a load failure to a skip reported in the LoadReport, and the fsync-policy options are accepted and are no-ops here — every Append is one committed transaction, so sqlite's commit cadence already is per-append and there is no buffer to defer. The database file is created 0600 and its directory 0700, matching jsonl's rule (ADR 0011 §5); the -wal and -shm side files are SQLite's own and share the main file's directory.
Example ¶
Open a sessions database, write one turn's worth of entries, read the session back. ":memory:" keeps the example self-contained; a path gives the same answers on disk.
package main
import (
"context"
"fmt"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/thread/sqlite"
)
func main() {
st, err := sqlite.Open(":memory:")
if err != nil {
fmt.Println(err)
return
}
ctx := context.Background()
created := time.Date(2026, 9, 29, 9, 0, 0, 0, time.UTC)
if err := st.Create(ctx, thread.Header{ID: "s_demo", Created: created}); err != nil {
fmt.Println(err)
return
}
if err := st.Append(ctx, "s_demo",
thread.MessageEntry{
ID: "e_1", Created: created.Add(time.Second), Message: weft.User("Where is order 1234?"),
},
thread.TurnEntry{
ID: "e_2", Created: created.Add(2 * time.Second),
RunID: "s_demo-t1", StopReason: weft.StopEndTurn, Steps: 1,
},
); err != nil {
fmt.Println(err)
return
}
h, entries, report, err := st.Load(ctx, "s_demo")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(h.ID, len(entries), entries[0].(thread.MessageEntry).Message.Text(), report == nil)
}
Output: s_demo 2 Where is order 1234? true
Example (SecondWriter) ¶
The one-writer rule is per session and crosses processes: a second Storage over the same file — here in the same process, the cheapest way to show it — is refused with thread.ErrLocked until the holder lets go, while Load keeps answering: readers never lock.
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/weftgo/weft"
"github.com/weftgo/weft/thread"
"github.com/weftgo/weft/thread/sqlite"
)
func main() {
dir, err := os.MkdirTemp("", "weft-sqlite-example")
if err != nil {
fmt.Println(err)
return
}
defer func() { _ = os.RemoveAll(dir) }()
ctx := context.Background()
path := filepath.Join(dir, "sessions.db")
holder, err := sqlite.Open(path)
if err != nil {
fmt.Println(err)
return
}
if err := holder.Create(ctx, thread.Header{ID: "s_demo", Created: time.Now().UTC()}); err != nil {
fmt.Println(err)
return
}
second, err := sqlite.Open(path)
if err != nil {
fmt.Println(err)
return
}
err = second.Append(ctx, "s_demo", thread.MessageEntry{
ID: "e_1", Created: time.Now().UTC(), Message: weft.User("second writer"),
})
fmt.Println(errors.Is(err, thread.ErrLocked))
}
Output: true
Types ¶
This section is empty.