CARLOS rastrillo docs

๐Ÿค– Data

Your app keeps its data in one SQLite file, reached through one *gorm.DB. This page covers the models you write and the handle db.Open gives you. Migrations covers changing the schema afterwards, and Scoping covers keeping one user's rows away from another's.

Models are plain GORM structs

No base type, no embedding, nothing to inherit:

type User struct {
	ID           int64
	Email        string `gorm:"uniqueIndex"`
	PasswordHash string
}

type Note struct {
	ID        int64
	UserID    int64 `gorm:"index"`
	Title     string
	Body      string
	UpdatedAt time.Time
}

UserID is the owner column. It is an ordinary field with an ordinary index. What makes it special is that every query on Note goes through the owner filter โ€” see Scoping.

Models live in internal/<app>/models.go, and a Models slice beside them is what rastrillo migration generate diffs against your migrations.

Opening the database

d, err := db.Open("notes.db", logger)
if err != nil {
	return err
}
defer d.Close()

d.G is the *gorm.DB your handlers use. Everything else about db.Open is the stuff that is easy to get subtly wrong, so it is worth knowing what it did for you.

One file, two pools

Writes go through a pool capped at one connection, because SQLite allows one writer and queueing inside the pool beats getting SQLITE_BUSY at the call site. Reads go through a pool of several, sized to the CPU count with a floor of four. WAL supports many readers, and serialising them behind a single connection turns an open *sql.Rows plus any second query into a silent deadlock.

dbresolver routes each statement to the right pool, so you never pick one.

The DSN

?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)

busy_timeout is set before journal_mode=WAL. The other order crashes under concurrent open. Foreign keys are on. Open also pings eagerly so the file exists on disk from boot, which is what keeps hibernation replication happy.

Two settings worth knowing

GORM is opened with TranslateError: true, so driver constraint errors become GORM's portable sentinels and errors.Is(err, gorm.ErrDuplicatedKey) works. Without that flag GORM never calls the dialector's Translate at all.

The logger ignores ErrRecordNotFound. A scoped by-id miss is the ordinary 404 path and happens on every not-yours URL, so it is control flow, not something worth a log line per hit.

Times are stored UTC.

Getting a *sql.DB out

Some packages want database/sql. sessions is the first one you will hit:

writer, err := d.G.DB()
if err != nil {
	return err
}
sess, err := sessions.New(sessions.Config{DB: writer, Origin: origin, Logger: logger})

d.Writer() returns the same write pool directly, and it is the clearer call when you specifically need the writer. migrate.Apply takes it that way, because it pins one connection for a whole run: PRAGMA foreign_keys is per-connection, and a table rebuild has to toggle it outside the transaction.

d.Close() closes both pools.

Never import a second SQLite driver

// Do not do this.
import _ "github.com/glebarez/sqlite"
import _ "gorm.io/driver/sqlite"

glebarez/sqlite registers the driver name sqlite, which modernc.org/sqlite already registers. A binary containing both it and rastrillo/gormlite panics at init, before any of your code runs, with a duplicate-driver message that does not obviously point at either import.

gorm.io/driver/sqlite is the cgo one, which defeats a pure-Go build.

You should not need to name a SQLite driver in your app at all. db.Open wires gormlite over modernc.org/sqlite for you.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.