CodeSampleX

Beispiel

gorm.io/gorm v1.31.2: Run GORM on SQLite with CGO_ENABLED=0, past go-sqlite3's runtime stub error

Verifiziertes Beispiel für golang gorm.io/gorm v1.31.2: Run GORM on SQLite with CGO_ENABLED=0, past go-sqlite3's runtime stub error. Der Vertrag lief auf go…

sha256:c3632f2f8dc28bb7ef59c80bcd728225b10ed65370fcd4c71af75032848b2f02

Dieses Netzwerk bietet eine Sache: ein Sample, das baut. Es hat es in einer Sandbox ausgeführt und die signierte Quittung behalten. Es bewertet nichts und garantiert nichts — ob derselbe Code bei Ihnen baut, hat es nicht gemessen. Wie viele verschiedene Signaturschlüssel eine bestandene Vertragsquittung eingereicht haben. Einer ist der Autor allein; mehr als einer heißt, jemand anderes hat es auch gebaut. Ein Schlüssel wird selbst erzeugt und hat keine registrierte Identität dahinter — gezählt werden Schlüssel, nicht Personen. MIT-0

Ausführungsbelege

Die deklarierte Umgebung und die signierten Läufe stehen getrennt, damit Sie genau sehen, was dieses Sample ausgeführt hat und wo.

Beleggrundlage
Signierter Vertrag bestanden
Verifizierungsbelege
3
Signaturschlüssel, die es gebaut haben
2
Deklarierte Umgebung go 1.26 linux · musl x64 go 1.26 go go

Umgebungen der Verifizierungsläufe

Umgebung Contract Stufen Lauf
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

Fall

HOW
Ziel
Run GORM on SQLite with CGO_ENABLED=0, past go-sqlite3's runtime stub error
Pakete
Symbole
  • 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
Umgebung
go 1.26
Erstellt
2026-08-14T13:23:54Z

Contract

  1. 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
  2. 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
  3. assert opening that cgo driver fails only at connection time, with go-sqlite3's Binary was compiled with CGO_ENABLED=0 stub message
  4. assert sql.Open on sqlite3 returns no error at all, because it does not connect, and that Ping is where the stub surfaces
  5. 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
  6. 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
  7. assert Updates with a struct writes the non-zero field and silently omits false and 0, returning no error and RowsAffected 1
  8. assert Updates with a map writes those same zero values, since a map has no zero value to be confused by
  9. assert Select names the columns that force a struct's zero values through
  10. assert Update with an explicit value writes a literal zero
  11. assert First on a missing row returns gorm.ErrRecordNotFound while Find returns a nil error with RowsAffected 0
  12. 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
  13. 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
  14. assert TranslateError makes that same conflict match gorm.ErrDuplicatedKey and replaces the driver error, so the constraint message is gone
  15. 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

Dateien

  • csx.json
  • go.mod
  • go.sum
  • src/cgo_off.go
  • src/cgo_on.go
  • src/store.go
  • test/main.go

Quellartefakt herunterladen (tar.gz)

Quelltext

csx.json
{"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"}
go.mod
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
)
go.sum
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=
src/cgo_off.go
//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
src/cgo_on.go
//go:build cgo

package store

const CgoEnabled = true
src/store.go
// 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 ""
}
test/main.go
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)
	}
}

Ursprungs-Seeder

anonymous