sqlite

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: MIT Imports: 23 Imported by: 0

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

View Source
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

func Open(path string, opts ...thread.OpenOption) (thread.Storage, error)

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.

Jump to

Keyboard shortcuts

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