Sample
gorm.io/gorm v1.31.2: Run GORM on SQLite with CGO_ENABLED=0, past go-sqlite3's runtime stub error
Verified sample for golang gorm.io/gorm v1.31.2: Run GORM on SQLite with CGO_ENABLED=0, past go-sqlite3's runtime stub error. The contract ran on go 1.26 …
sha256:c3632f2f8dc28bb7ef59c80bcd728225b10ed65370fcd4c71af75032848b2f02
This network offers one thing: a sample that builds. It ran the sample in a sandbox and kept the signed receipt. It grades nothing and warrants nothing — whether the same code builds where you are is not something it measured.
How many distinct signing keys filed a passing contract receipt. One is the author alone; more than one means somebody else built it too. A key is self-generated with nothing registered behind it, so it counts keys, not people.
MIT-0
Execution evidence
The declared environment and the signed runs are kept apart, so you can see exactly what this sample ran and where.
- Evidence basis
- Signed contract pass
- Verification receipts
- 3
- Signing keys that built it
- 2
Declared environment
go 1.26 linux · musl x64 go 1.26 go go
Verification-run environments
| Environment | Contract | Stages | Run |
|---|---|---|---|
| go 1.26 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-14 |
| go 1.26 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-18 |
| go 1.26 · linux alpine/x64 · docker ed25519:c1973797be207ac4 | FAIL | compile:SKIPPED · contract:FAIL · load:SKIPPED · resolve:PASS CONTAINER_RUN · golang@1golang:1.26-alpine@sha256:28d89ee9cc0f… |
2026-09-07 |
Case
HOW- Goal
- Run GORM on SQLite with CGO_ENABLED=0, past go-sqlite3's runtime stub error
- Packages
- Symbols
-
- sqlite.Open
- gorm.Open
- gorm.DeletedAt
- gorm.ErrRecordNotFound
- gorm.ErrDuplicatedKey
- gorm.Config.TranslateError
- sql.Open
- sql.Drivers
- DB.Ping
- DB.AutoMigrate
- DB.Updates
- DB.Update
- DB.Select
- DB.First
- DB.Find
- DB.Unscoped
- DB.Create
- Environment
- go 1.26
- Created
- 2026-08-14T13:23:54Z
Contract
- assert the build has cgo off and that there is no C compiler on PATH, so rebuilding with CGO_ENABLED=1 has nothing to invoke
- assert gorm.io/driver/sqlite still compiles and links with cgo off, since the contract binary imports it, and that its stub registers the database/sql driver name sqlite3 alongside the pure-Go engine's sqlite
- assert opening that cgo driver fails only at connection time, with go-sqlite3's Binary was compiled with CGO_ENABLED=0 stub message
- assert sql.Open on sqlite3 returns no error at all, because it does not connect, and that Ping is where the stub surfaces
- assert the linked pure-Go engine reports SQLite 3.53.3, so the forced modernc.org/sqlite pin cannot drift back to what minimal version selection would pick
- assert AutoMigrate creates the table and its deleted_at column, that running it again on an unchanged model is a no-op rather than an error, and that Create writes the auto-increment key back into the struct
- assert Updates with a struct writes the non-zero field and silently omits false and 0, returning no error and RowsAffected 1
- assert Updates with a map writes those same zero values, since a map has no zero value to be confused by
- assert Select names the columns that force a struct's zero values through
- assert Update with an explicit value writes a literal zero
- assert First on a missing row returns gorm.ErrRecordNotFound while Find returns a nil error with RowsAffected 0
- assert gorm.DeletedAt makes Delete an UPDATE: Find and First skip the row, Unscoped finds it with DeletedAt.Valid set, and a raw count shows the row still in the table
- assert reusing the primary key of a soft-deleted row fails with SQLite's own UNIQUE constraint failed: users.id message, which does not match gorm.ErrDuplicatedKey by default
- assert TranslateError makes that same conflict match gorm.ErrDuplicatedKey and replaces the driver error, so the constraint message is gone
- assert two connections to :memory: are two separate databases, so the migrated table is missing on the second one unless the pool is pinned to a single connection
Files
- csx.json
- go.mod
- go.sum
- src/cgo_off.go
- src/cgo_on.go
- src/store.go
- test/main.go
Source
{"case":{"caseId":"case:sha256:0a72c4070aae8939a081008aff13a68818dcd3553ee062c366ce482505bd086c","constraints":{"libc":"musl","runtime":"go"},"contract":["assert the build has cgo off and that there is no C compiler on PATH, so rebuilding with CGO_ENABLED=1 has nothing to invoke","assert gorm.io/driver/sqlite still compiles and links with cgo off, since the contract binary imports it, and that its stub registers the database/sql driver name sqlite3 alongside the pure-Go engine's sqlite","assert opening that cgo driver fails only at connection time, with go-sqlite3's Binary was compiled with CGO_ENABLED=0 stub message","assert sql.Open on sqlite3 returns no error at all, because it does not connect, and that Ping is where the stub surfaces","assert the linked pure-Go engine reports SQLite 3.53.3, so the forced modernc.org/sqlite pin cannot drift back to what minimal version selection would pick","assert AutoMigrate creates the table and its deleted_at column, that running it again on an unchanged model is a no-op rather than an error, and that Create writes the auto-increment key back into the struct","assert Updates with a struct writes the non-zero field and silently omits false and 0, returning no error and RowsAffected 1","assert Updates with a map writes those same zero values, since a map has no zero value to be confused by","assert Select names the columns that force a struct's zero values through","assert Update with an explicit value writes a literal zero","assert First on a missing row returns gorm.ErrRecordNotFound while Find returns a nil error with RowsAffected 0","assert gorm.DeletedAt makes Delete an UPDATE: Find and First skip the row, Unscoped finds it with DeletedAt.Valid set, and a raw count shows the row still in the table","assert reusing the primary key of a soft-deleted row fails with SQLite's own UNIQUE constraint failed: users.id message, which does not match gorm.ErrDuplicatedKey by default","assert TranslateError makes that same conflict match gorm.ErrDuplicatedKey and replaces the driver error, so the constraint message is gone","assert two connections to :memory: are two separate databases, so the migrated table is missing on the second one unless the pool is pinned to a single connection"],"goal":"Run GORM on SQLite with CGO_ENABLED=0, past go-sqlite3's runtime stub error","kind":"HOW","packages":["pkg:golang/gorm.io/gorm@1.31.2","pkg:golang/github.com/glebarez/sqlite@1.11.0","pkg:golang/modernc.org/sqlite@1.56.0","pkg:golang/gorm.io/driver/sqlite@1.6.0","pkg:golang/github.com/mattn/go-sqlite3@1.14.49"],"schemaVersion":1,"symbols":["sqlite.Open","gorm.Open","gorm.DeletedAt","gorm.ErrRecordNotFound","gorm.ErrDuplicatedKey","gorm.Config.TranslateError","sql.Open","sql.Drivers","DB.Ping","DB.AutoMigrate","DB.Updates","DB.Update","DB.Select","DB.First","DB.Find","DB.Unscoped","DB.Create"]},"contractCommand":["go","run","./test"],"environment":{"arch":"x64","ecosystem":"golang","executionContext":"go","language":"go","libc":"musl","os":"linux","packageManager":"go","runtime":"go","runtimeVersion":"1.26","schemaVersion":1},"license":"MIT-0","packages":["pkg:golang/gorm.io/gorm@1.31.2","pkg:golang/github.com/glebarez/sqlite@1.11.0","pkg:golang/modernc.org/sqlite@1.56.0","pkg:golang/gorm.io/driver/sqlite@1.6.0","pkg:golang/github.com/mattn/go-sqlite3@1.14.49"],"schemaVersion":1,"symbols":["sqlite.Open","gorm.Open","gorm.DeletedAt","gorm.ErrRecordNotFound","gorm.ErrDuplicatedKey","gorm.Config.TranslateError","sql.Open","sql.Drivers","DB.Ping","DB.AutoMigrate","DB.Updates","DB.Update","DB.Select","DB.First","DB.Find","DB.Unscoped","DB.Create"],"verifierAdapter":"golang@1"}
module codesamplex.dev/sample/gogormsqlite
go 1.25.0
require (
github.com/glebarez/sqlite v1.11.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.2
)
// glebarez/sqlite v1.11.0 is the latest release and dates from March 2024, so
// its own go.mod asks for modernc.org/sqlite v1.23.1. That is what minimal
// version selection settles on if you just run `go get github.com/glebarez/
// sqlite`, and it was measured to link SQLite 3.41.2, released in April 2023.
// Forcing the current engine gives 3.53.3, which the contract asserts so the
// pin cannot drift back unnoticed. modernc.org/libc follows from the engine
// and is not a separate choice.
//
// mattn/go-sqlite3 arrives under gorm.io/driver/sqlite, which the contract
// imports only to measure that the cgo driver builds here and then fails at
// the first connection. It is pinned to the current release so the stub
// behaviour being recorded is the current one, not the 2024 one the driver
// asks for.
require (
github.com/mattn/go-sqlite3 v1.14.49 // indirect
modernc.org/sqlite v1.56.0 // indirect
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.20.0 // indirect
modernc.org/libc v1.74.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
//go:build !cgo
package store
// CgoEnabled is the compiler's own answer to "was this built with cgo", taken
// from the build tag rather than from an environment variable that may have
// been set after the fact.
const CgoEnabled = false
//go:build cgo
package store
const CgoEnabled = true
// Package store runs GORM against SQLite in an image with no C toolchain.
//
// The answer everyone finds first is gorm.io/driver/sqlite, which wraps
// github.com/mattn/go-sqlite3. That is a cgo package, so the obvious guess is
// that it refuses to build when CGO_ENABLED=0. Measured here, it does not:
// mattn compiles a cgo-less stub instead, so the import builds, the binary
// links, and the stub still registers itself with database/sql under the name
// "sqlite3". Nothing objects until something actually connects, and then the
// call returns
//
// Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub
//
// This is why the failure shows up in a container and not in CI: the build
// that produced the image succeeded. sql.Open hides it one step further,
// since it only records the driver name — Ping is where it surfaces.
//
// github.com/glebarez/sqlite is the same GORM dialector over a pure-Go engine
// (modernc.org/sqlite, SQLite transpiled to Go). It needs no C, and it
// registers under the driver name "sqlite". A snippet written for mattn says
// "sqlite3" and so reaches the stub rather than this engine, even when both
// are linked into the same binary.
package store
import (
"os/exec"
"time"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// User carries the two field types this contract is about: scalars with
// meaningful zero values (Active, Count) and gorm.DeletedAt, which is what
// turns every query on this model into a soft-delete query.
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Count int
Active bool
CreatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// Open returns a GORM handle on an in-memory database that behaves like one
// database.
//
// Without SetMaxOpenConns(1) it would not. ":memory:" names a private
// database per connection, and database/sql hands out connections from a
// pool, so AutoMigrate can run on one connection and the next query can land
// on a fresh, empty one — the "no such table" that gets blamed on GORM.
// OpenPooled leaves the limit off so the contract can show that happening.
func Open(dsn string) (*gorm.DB, error) {
db, err := OpenPooled(dsn)
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(1)
return db, nil
}
// OpenPooled is Open without the connection limit.
func OpenPooled(dsn string) (*gorm.DB, error) {
return open(dsn, false)
}
// OpenTranslating switches on GORM's error translation, which routes driver
// errors through the dialector's Translate method. It is off by default and
// it is not free: Translate returns the sentinel in place of the original
// error rather than wrapping it, so turning it on buys errors.Is at the cost
// of the message naming the column.
func OpenTranslating(dsn string) (*gorm.DB, error) {
db, err := open(dsn, true)
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(1)
return db, nil
}
func open(dsn string, translateError bool) (*gorm.DB, error) {
return gorm.Open(sqlite.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
TranslateError: translateError,
})
}
// CCompiler reports the C compiler on PATH, or "" when there is none. It
// matters because rebuilding with CGO_ENABLED=1 is the usual advice for the
// stub error above, and here there is nothing for cgo to invoke.
func CCompiler() string {
for _, name := range []string{"gcc", "cc", "clang"} {
if path, err := exec.LookPath(name); err == nil {
return path
}
}
return ""
}
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"os"
"slices"
"strings"
cgosqlite "gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"codesamplex.dev/sample/gogormsqlite/src"
)
func main() {
// The environment, taken from the compiler and the filesystem rather than
// from an environment variable that may have been changed after the build.
check(!store.CgoEnabled, "built with cgo, so this seed is not testing what it claims to")
check(store.CCompiler() == "", "found a C compiler at %q, so cgo was avoidable here", store.CCompiler())
// The trap, measured rather than assumed. gorm.io/driver/sqlite wraps the
// cgo package mattn/go-sqlite3, and the expectation is that it cannot be
// built with cgo off. It can: mattn compiles a stub, and this binary is
// the evidence, because it imports the driver above and links. The stub
// registers "sqlite3" with database/sql just like the real thing, so the
// name being present proves nothing about the driver working.
drivers := sql.Drivers()
check(slices.Contains(drivers, "sqlite3"),
"the cgo driver's stub should still register sqlite3, got %v", drivers)
check(slices.Contains(drivers, "sqlite"),
"expected the pure-Go engine registered as sqlite, got %v", drivers)
// It fails only when something connects, and this is the message people
// paste into a search box.
silent := &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}
_, err := gorm.Open(cgosqlite.Open(":memory:"), silent)
check(err != nil, "the cgo driver must not open a database with cgo off")
check(strings.Contains(err.Error(), "CGO_ENABLED=0") &&
strings.Contains(err.Error(), "requires cgo to work"),
"expected go-sqlite3's stub error, got %q", err.Error())
// database/sql defers it one step further than that: sql.Open only records
// the driver name, so nothing is wrong until Ping opens a connection.
stub, err := sql.Open("sqlite3", ":memory:")
check(err == nil, "sql.Open does not connect and so cannot report the stub, got %v", err)
defer stub.Close()
pingErr := stub.Ping()
check(pingErr != nil && strings.Contains(pingErr.Error(), "This is a stub"),
"expected Ping to surface the stub, got %v", pingErr)
// The working engine is in this same binary, reachable only under the name
// "sqlite". A snippet copied from a mattn answer says "sqlite3" and lands
// on the stub above instead.
db, err := store.Open(":memory:")
must(err, "open")
// The engine is whatever modernc.org/sqlite was pinned to, and minimal
// version selection would have picked a much older one. Asserting it
// makes the indirect pin in go.mod a fact rather than a preference.
var engine string
must(db.Raw("select sqlite_version()").Scan(&engine).Error, "sqlite_version")
check(engine == "3.53.3", "expected the pinned engine, got %q", engine)
// AutoMigrate creates the table and can be run again on an unchanged
// model without error. It is DDL reconciliation rather than a migration
// history: it adds what is missing, so running it twice is not a second
// migration to be guarded against.
migrator := db.Migrator()
check(!migrator.HasTable(&store.User{}), "users existed before AutoMigrate")
must(db.AutoMigrate(&store.User{}), "AutoMigrate")
check(migrator.HasTable(&store.User{}), "AutoMigrate did not create users")
check(migrator.HasColumn(&store.User{}, "deleted_at"), "no deleted_at column")
must(db.AutoMigrate(&store.User{}), "second AutoMigrate, which should be a no-op")
// Create fills the auto-increment primary key back into the struct.
u := store.User{Name: "ada", Count: 7, Active: true}
must(db.Create(&u).Error, "create")
check(u.ID != 0, "Create should have written the primary key back")
// The single most reported GORM surprise. Updates with a STRUCT skips
// every field holding its zero value, so this writes the name and
// silently drops Active=false and Count=0. No error, RowsAffected 1 —
// there is nothing in the result to tell you two thirds of the update
// went missing.
res := db.Model(&u).Updates(store.User{Name: "ada-renamed", Active: false, Count: 0})
must(res.Error, "struct Updates")
check(res.RowsAffected == 1, "struct Updates touched %d rows", res.RowsAffected)
got := reload(db, u.ID)
check(got.Name == "ada-renamed", "the non-zero field should have been written, got %q", got.Name)
check(got.Active, "struct Updates must not write false")
check(got.Count == 7, "struct Updates must not write 0, got %d", got.Count)
// A MAP has no zero value to be confused by: every key is written,
// including false and 0. This is the way out when the update is driven
// by data rather than by a struct.
must(db.Model(&got).Updates(map[string]any{"active": false, "count": 0}).Error, "map Updates")
got = reload(db, u.ID)
check(!got.Active && got.Count == 0, "map Updates must write zero values, got %+v", got)
// Select names the columns explicitly and is the way out when you do
// have a struct. Restore first, so the write is a real change.
must(db.Model(&got).Updates(map[string]any{"active": true, "count": 7}).Error, "restore")
check(reload(db, u.ID).Active, "restore did not take")
must(db.Model(&got).Select("active", "count").Updates(store.User{Active: false, Count: 0}).Error,
"Select+struct Updates")
got = reload(db, u.ID)
check(!got.Active && got.Count == 0, "Select must force zero values through, got %+v", got)
// Update (singular) takes the value as given, which is the third way out.
must(db.Model(&got).Update("count", 5).Error, "Update to 5")
check(reload(db, u.ID).Count == 5, "Update should write the literal value")
must(db.Model(&got).Update("count", 0).Error, "Update to 0")
check(reload(db, u.ID).Count == 0, "Update must write an explicit zero")
// First reports a missing row as an error. Find does not: an empty
// result is a successful query with RowsAffected 0, which is why
// checking err after Find never catches "nothing matched".
var missing store.User
err = db.First(&missing, 424242).Error
check(errors.Is(err, gorm.ErrRecordNotFound), "expected ErrRecordNotFound, got %v", err)
var none []store.User
res = db.Where("id = ?", 424242).Find(&none)
check(res.Error == nil, "Find on zero rows is not an error, got %v", res.Error)
check(res.RowsAffected == 0 && len(none) == 0, "Find returned %d rows", res.RowsAffected)
// gorm.DeletedAt on the model turns Delete into an UPDATE and adds
// "deleted_at IS NULL" to every later query on it. Unscoped drops that
// condition, and the row was never physically removed.
must(db.Delete(&got).Error, "delete")
var live []store.User
must(db.Find(&live).Error, "find")
check(len(live) == 0, "soft-deleted row still visible to Find: %+v", live)
err = db.First(&missing, got.ID).Error
check(errors.Is(err, gorm.ErrRecordNotFound), "soft-deleted First should be not-found, got %v", err)
var all []store.User
must(db.Unscoped().Find(&all).Error, "unscoped find")
check(len(all) == 1, "Unscoped should see the soft-deleted row, got %d", len(all))
check(all[0].DeletedAt.Valid, "deleted_at should be set, got %+v", all[0].DeletedAt)
var physical int64
must(db.Raw("select count(*) from users").Scan(&physical).Error, "raw count")
check(physical == 1, "soft delete must leave the row in the table, found %d", physical)
// Soft delete frees nothing as far as constraints are concerned. The row
// is still physically there, so its primary key is still taken, and the
// error carries SQLite's own constraint message rather than anything GORM
// invented.
err = db.Create(&store.User{ID: got.ID, Name: "grace"}).Error
check(err != nil, "duplicate primary key must fail even when the row is soft-deleted")
check(strings.Contains(err.Error(), "UNIQUE constraint failed: users.id"),
"expected the SQLite constraint message, got %q", err.Error())
// Error translation is off by default, so that stays a driver error and
// errors.Is against GORM's sentinel does not match it.
check(!errors.Is(err, gorm.ErrDuplicatedKey), "unexpected translation to ErrDuplicatedKey: %v", err)
duplicateKeyUnderTranslation()
memoryIsPerConnection()
fmt.Println("contract ok")
}
// gorm.Config{TranslateError: true} makes errors.Is(err, gorm.ErrDuplicatedKey)
// work, which is the portable way to catch a conflict. The trade is real and
// goes the other way: this dialector's Translate returns the sentinel INSTEAD
// of the driver error rather than wrapping it, so the message naming the
// failing constraint is gone from the error a handler logs.
func duplicateKeyUnderTranslation() {
db, err := store.OpenTranslating(":memory:")
must(err, "open translating")
must(db.AutoMigrate(&store.User{}), "translating AutoMigrate")
must(db.Create(&store.User{ID: 1, Name: "ada"}).Error, "translating create")
err = db.Create(&store.User{ID: 1, Name: "grace"}).Error
check(errors.Is(err, gorm.ErrDuplicatedKey), "expected ErrDuplicatedKey, got %v", err)
check(!strings.Contains(err.Error(), "UNIQUE constraint failed"),
"translation was expected to replace the driver message, got %q", err.Error())
}
// ":memory:" names a private database per connection, and database/sql hands
// out connections from a pool, so a second connection is a second empty
// database. Two explicit connections make that deterministic instead of a
// race: the migrated table exists on the first one only. store.Open pins the
// pool to a single connection for exactly this reason.
func memoryIsPerConnection() {
db, err := store.OpenPooled(":memory:")
must(err, "open pooled")
must(db.AutoMigrate(&store.User{}), "pooled AutoMigrate")
sqlDB, err := db.DB()
must(err, "sql handle")
ctx := context.Background()
first, err := sqlDB.Conn(ctx)
must(err, "first conn")
defer first.Close()
second, err := sqlDB.Conn(ctx)
must(err, "second conn")
defer second.Close()
const q = "select count(*) from sqlite_master where type = 'table' and name = 'users'"
var onFirst, onSecond int
must(first.QueryRowContext(ctx, q).Scan(&onFirst), "query first")
must(second.QueryRowContext(ctx, q).Scan(&onSecond), "query second")
check(onFirst == 1, "the migrated connection should have users, got %d", onFirst)
check(onSecond == 0, "a second connection should be a different empty database, got %d", onSecond)
}
func reload(db *gorm.DB, id uint) store.User {
var out store.User
must(db.First(&out, id).Error, fmt.Sprintf("reload %d", id))
return out
}
func must(err error, what string) {
check(err == nil, "%s: %v", what, err)
}
func check(ok bool, format string, args ...any) {
if !ok {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}
}