# GORM Admin & Architecture Guide

This guide is for engineers, architects, and library integrators who need to understand **how GORM works internally**, how to configure it for production, how to extend it, and how to operate applications that depend on it. It complements the [User Guide](/go-gorm/gorm/user-guide/), which covers day-to-day API usage.

GORM (`gorm.io/gorm`) is a pure-Go ORM library — there is no server, daemon, CLI, or background process to deploy. "Operating" GORM means configuring it correctly inside your own Go application and tuning the database connection it manages.

---

## 1. Executive Summary

| Property | Value |
|----------|-------|
| Module path | `gorm.io/gorm` |
| Language | Go (`go 1.18` minimum; `slog` logger requires 1.21+) |
| Type | Embeddable ORM library |
| Runtime dependencies | `github.com/jinzhu/inflection`, `github.com/jinzhu/now`, `golang.org/x/text` |
| Database support | Via separate driver packages (`gorm.io/driver/*`): MySQL, PostgreSQL, SQLite, SQL Server, GaussDB, … |
| License | MIT |
| Source layout | Core package + `callbacks/`, `clause/`, `schema/`, `migrator/`, `logger/`, `utils/`, `internal/` |
| Analyzed commit | `d0ee5e2` (2026-05-11) |

GORM's design centers on three ideas:

1. **Structs are the schema.** Models are plain Go structs; reflection plus `gorm` tags drive table/column mapping, constraints, indexes, and relationships.
2. **Everything is a callback pipeline.** Create, Query, Update, Delete, Row, and Raw operations each run an ordered list of registered callback functions. This is the primary extension point.
3. **SQL is built from composable clauses.** Each statement is assembled from `clause.Expression` objects (`SELECT`, `WHERE`, `LIMIT`, `ON CONFLICT`, …) that dialectors render to database-specific SQL.

---

## 2. Architecture

### 2.1 Component overview

```
        Your application code
                 │
        ┌────────▼─────────┐
        │   *gorm.DB       │  chainable + finisher API (gorm.go,
        │  (Session/clone) │  chainable_api.go, finisher_api.go,
        └────────┬─────────┘  generics.go, association.go)
                 │ builds
        ┌────────▼─────────┐
        │   Statement      │  per-operation state: Model, Schema,
        │  (statement.go)  │  Clauses, Vars, ReflectValue, SQL
        └────────┬─────────┘
                 │ executed by
        ┌────────▼─────────┐      ┌──────────────────────┐
        │  callbacks       │◄────►│  schema.Schema cache  │
        │ (processor/      │      │  (schema/*.go)        │
        │  callback)       │      │  fields, relations,   │
        │  callbacks/*.go  │      │  indexes, constraints │
        └────────┬─────────┘      └──────────────────────┘
                 │ builds SQL from
        ┌────────▼─────────┐
        │  clause.*        │  SELECT/WHERE/JOIN/LIMIT/ON CONFLICT/…
        │  (clause/*.go)   │
        └────────┬─────────┘
                 │ rendered + executed by
        ┌────────▼─────────┐      ┌──────────────────────┐
        │  Dialector       │      │  ConnPool             │
        │ (driver package) │─────►│  database/sql pool    │
        │  Explain/QuoteTo/ │      │  or PreparedStmtDB    │
        │  BindVarTo/...   │      │  (prepare_stmt.go)    │
        └──────────────────┘      └──────────────────────┘
                                            │
                                       Your database
```

### 2.2 The core types

| Type | File | Role |
|------|------|------|
| `DB` | `gorm.go` | The handle users interact with. Wraps `*Config`, `Error`, `RowsAffected`, `Statement`, and an internal `clone` counter for chain semantics. |
| `Config` | `gorm.go` | All global behavior knobs (see [Section 4](#4-configuration-reference)). |
| `Session` | `gorm.go` | A set of per-scope overrides applied via `db.Session(...)`. |
| `Statement` | `statement.go` | Mutable per-operation builder: the model, parsed schema, accumulated clauses, bind vars, reflect value, and the `strings.Builder` for SQL. |
| `Dialector` | `interfaces.go` | The driver contract — `Name`, `Initialize`, `Migrator`, `DataTypeOf`, `BindVarTo`, `QuoteTo`, `Explain`. |
| `ConnPool` | `interfaces.go` | The minimal `database/sql`-style execution surface (`ExecContext`, `QueryContext`, …). |
| `Schema` | `schema/schema.go` | Parsed, cached representation of a model: fields, relationships, indexes, constraints. |

### 2.3 The clone / chain model

`DB` methods return a `*DB`, but they do not mutate the original handle. The `clone` field (set to `1` at `Open`) governs this: when `clone > 0`, `getInstance()` produces a fresh `*DB` with a new (or cloned) `Statement` before applying the call. This is what makes the following safe:

```go
base := db.Where("active = ?", true) // base carries one WHERE
adults := base.Where("age >= ?", 18) // a distinct chain
minors := base.Where("age < ?", 18)  // another distinct chain — base is untouched
```

> [!IMPORTANT]
> A `*gorm.DB` obtained from a finisher should not be reused as a "clean" handle — it carries the accumulated statement. For a fresh, condition-free handle, start a new session: `db.Session(&gorm.Session{NewDB: true})` (or simply use the root `db`).

### 2.4 The callback pipeline (the heart of GORM)

Every high-level operation dispatches to one of six **processors** — `create`, `query`, `update`, `delete`, `row`, `raw` — each holding an ordered list of callbacks. `RegisterDefaultCallbacks` (in `callbacks/callbacks.go`) wires the defaults:

| Processor | Default callback order |
|-----------|------------------------|
| **Create** | `begin_transaction` → `before_create` → `save_before_associations` → `create` → `save_after_associations` → `after_create` → `commit_or_rollback_transaction` |
| **Query** | `query` → `preload` → `after_query` |
| **Update** | `begin_transaction` → `setup_reflect_value` → `before_update` → `save_before_associations` → `update` → `save_after_associations` → `after_update` → `commit_or_rollback_transaction` |
| **Delete** | `begin_transaction` → `before_delete` → `delete_before_associations` → `delete` → `after_delete` → `commit_or_rollback_transaction` |
| **Row** | `row` |
| **Raw** | `raw` |

The `begin_transaction` / `commit_or_rollback_transaction` pair is only registered when `SkipDefaultTransaction` is false (via `processor.Match(enableTransaction)`), which is why every single write is, by default, atomic.

`processor.Execute` (`callbacks.go`) is the engine: it runs pending scopes, parses the model into a `Schema`, sets up the reflect value, runs each callback function in order, then emits a `Logger.Trace` line with the final SQL, elapsed time, rows affected, and any error.

### 2.5 Callback ordering algorithm

Callbacks register with optional `Before(name)` / `After(name)` constraints (and the special `"*"` wildcard for "first"/"last"). `sortCallbacks` (`callbacks.go`) performs a constraint-driven topological sort. Conflicting constraints (e.g. a cycle) produce errors like `conflicting callback %s with before %s`, and duplicate names emit warnings. This is how a plugin can insert itself, say, *after* `gorm:create` but *before* `gorm:after_create`.

### 2.6 SQL building via clauses

A `Statement` accumulates named clauses (`statement.AddClause`). At execution, callbacks call `statement.Build("SELECT", "FROM", "WHERE", ...)` to emit each clause in order. The `clause/` package defines every clause type — `Select`, `From`, `Where` (with `AndConditions`/`OrConditions`/`NotConditions`), `Joins`, `GroupBy`, `OrderBy`, `Limit`, `Locking`, `OnConflict`, `Returning`, `Insert`, `Values`, `Update`, `Set`, `Expr`, `NamedExpr`, `IN`, `Eq`, `With` (CTEs), etc. Each implements `clause.Expression` / `clause.Interface` and writes dialector-quoted SQL plus bind variables.

### 2.7 Schema parsing and caching

`schema.Parse` (`schema/schema.go`) reflects over a model once and caches the resulting `*Schema` in a `sync.Map` (`Config.cacheStore`), keyed by type. The parse resolves:

- **Fields** (`schema/field.go`) — data type, size, defaults, auto-time, serializers, read/write permissions, all from `ParseTagSetting`.
- **Relationships** (`schema/relationship.go`) — Belongs To, Has One, Has Many, Many To Many, and Polymorphic, with foreign-key/reference resolution.
- **Indexes** (`schema/index.go`) and **Constraints** (`schema/constraint.go`) — `CheckConstraint`, `UniqueConstraint`.
- **Naming** (`schema/naming.go`) — the `NamingStrategy` that converts Go names to table/column names.

> [!NOTE]
> Schema parsing is the most reflection-heavy step. Because results are cached per type, the cost is paid once per model per process. Several open performance issues (e.g. #7787) propose `singleflight` to de-duplicate concurrent first-time parses.

---

## 3. Requirements

| Requirement | Detail |
|-------------|--------|
| Go toolchain | 1.18+ for the core and the `gorm.G[T]` generics API; 1.21+ to use `logger.NewSlogLogger` |
| C compiler | Only if you choose the SQLite driver (cgo via `mattn/go-sqlite3`); MySQL/Postgres/SQL Server drivers are pure Go |
| Database server | Whatever your chosen driver targets; SQLite needs only a writable file path |
| Build tags | `slog.go` is guarded by `//go:build go1.21` |

GORM itself imposes no minimum hardware footprint beyond your application's; its memory use scales with the number of parsed model types and the configured connection pool size.

---

## 4. Configuration Reference

Every option lives on `gorm.Config` (`gorm.go`) and is passed to `gorm.Open`. Sessions (`gorm.Session`) can override a subset per-scope.

### 4.1 `gorm.Config` fields

| Field | Type | Default | Purpose |
|-------|------|---------|---------|
| `SkipDefaultTransaction` | `bool` | `false` | If true, single Create/Update/Delete are **not** wrapped in a transaction (faster, less safe). |
| `DefaultTransactionTimeout` | `time.Duration` | `0` | Applies a timeout context to transactions begun via `Begin`. |
| `DefaultContextTimeout` | `time.Duration` | `0` | Applies a timeout to each operation's context if none is set. |
| `NamingStrategy` | `schema.Namer` | `NamingStrategy{IdentifierMaxLength: 64}` | Controls table/column/index naming. |
| `FullSaveAssociations` | `bool` | `false` | If true, association writes upsert the full associated record, not just the FK. |
| `Logger` | `logger.Interface` | `logger.Default` (Warn level) | SQL/error logger. |
| `NowFunc` | `func() time.Time` | `time.Now().Local()` | Source of "now" for timestamps — override for UTC or test clocks. |
| `DryRun` | `bool` | `false` | Build SQL without executing it. |
| `PrepareStmt` | `bool` | `false` | Cache and reuse prepared statements (LRU + TTL). |
| `PrepareStmtMaxSize` | `int` | unbounded | Max cached prepared statements. |
| `PrepareStmtTTL` | `time.Duration` | `1h`-ish default | TTL for cached prepared statements. |
| `DisableAutomaticPing` | `bool` | `false` | Skip the connectivity `Ping()` after `Open`. |
| `DisableForeignKeyConstraintWhenMigrating` | `bool` | `false` | Don't emit FK constraints during `AutoMigrate`. |
| `IgnoreRelationshipsWhenMigrating` | `bool` | `false` | Don't auto-create related tables during migration. |
| `DisableNestedTransaction` | `bool` | `false` | Don't use save points for nested `Transaction` calls. |
| `AllowGlobalUpdate` | `bool` | `false` | Permit updates/deletes with no `WHERE` clause. |
| `QueryFields` | `bool` | `false` | `SELECT` explicit column names instead of `*`. |
| `CreateBatchSize` | `int` | `0` | Auto-batch `Create` on large slices. |
| `TranslateError` | `bool` | `false` | Map driver errors to GORM sentinels (`ErrDuplicatedKey`, etc.). |
| `PropagateUnscoped` | `bool` | `false` | Propagate `Unscoped` into nested statements. |
| `ClauseBuilders` | `map[string]clause.ClauseBuilder` | empty | Override how named clauses are built (advanced). |
| `ConnPool` | `ConnPool` | from dialector | Inject a custom connection pool. |
| `Dialector` | `Dialector` | from `Open` arg | The database driver. |
| `Plugins` | `map[string]Plugin` | empty | Registered plugins. |

> [!TIP]
> For a production HTTP service, a solid baseline is: `PrepareStmt: true`, `TranslateError: true`, a `Logger` configured to `Warn` with a `SlowThreshold`, and `NowFunc` returning UTC. Keep `SkipDefaultTransaction` at its default unless profiling shows the per-write transaction is a real bottleneck.

### 4.2 Naming strategy

`schema.NamingStrategy` (`schema/naming.go`) fields:

| Field | Effect |
|-------|--------|
| `TablePrefix` | Prepended to every table name (e.g. `"public."`). |
| `SingularTable` | If true, table names are singular (`user`, not `users`). |
| `NoLowerCase` | Disable snake_case lowercasing. |
| `NameReplacer` | A `strings.Replacer`-like hook to rewrite identifiers (e.g. `"CID" → "Cid"`). |
| `IdentifierMaxLength` | Truncate generated identifiers (default 64). |

```go
db, _ := gorm.Open(dialector, &gorm.Config{
	NamingStrategy: schema.NamingStrategy{
		TablePrefix:   "app_",
		SingularTable: false,
	},
})
```

### 4.3 Logger configuration

`logger.Config` (`logger/logger.go`):

| Field | Purpose |
|-------|---------|
| `LogLevel` | One of `Silent` (1), `Error` (2), `Warn` (3), `Info` (4). |
| `SlowThreshold` | Queries slower than this are logged at Warn as `SLOW SQL`. Default `200ms` on `logger.Default`. |
| `Colorful` | ANSI color output. |
| `IgnoreRecordNotFoundError` | Suppress logging of `ErrRecordNotFound`. |
| `ParameterizedQueries` | If true, the logger redacts bind values (logs `?`/`$n` instead of data) — useful for not leaking PII into logs. |

```go
newLogger := logger.New(
	log.New(os.Stdout, "\r\n", log.LstdFlags),
	logger.Config{
		SlowThreshold:             200 * time.Millisecond,
		LogLevel:                  logger.Warn,
		IgnoreRecordNotFoundError: true,
		ParameterizedQueries:      true,
		Colorful:                  false,
	},
)
db, _ := gorm.Open(dialector, &gorm.Config{Logger: newLogger})
```

On Go 1.21+, `logger.NewSlogLogger(slog.Default(), cfg)` routes GORM logs through structured `log/slog`, emitting `duration`, `sql`, and `rows` attributes.

> [!CAUTION]
> The logger interpolates bind variables into the SQL string for human readability (`logger.ExplainSQL`). That string is **for display only** — never feed it back to the database, and prefer `ParameterizedQueries: true` in environments where query logs may contain sensitive values.

---

## 5. Security

GORM is a library, so its security posture is mostly about how you *use* it. The relevant facts from the source:

### 5.1 SQL injection

- **Placeholders are safe.** `Where`, `Raw`, `Exec`, etc. parameterize `?` and named (`@name`) arguments through the driver's bind-variable mechanism (`Dialector.BindVarTo`). User input passed as an *argument* is never concatenated into SQL.
- **Identifiers and raw fragments are NOT escaped.** The first string argument to `Where("...")`, `Order(...)`, `Select(...)`, `Group(...)`, `Table(...)`, `Raw(...)`, and `Joins(...)` is treated as SQL. Never build these from untrusted input.

> [!WARNING]
> `db.Order(userInput)` and `db.Where(fmt.Sprintf("name = '%s'", userInput))` are injection vectors. Always pass user data as a **bind argument**: `db.Where("name = ?", userInput)`. For dynamic column/sort names, validate against an allow-list before interpolating.

### 5.2 The `ExplainSQL` warning

`logger.ExplainSQL` and `db.ToSQL` produce interpolated SQL strings explicitly documented as unsafe to execute (injection risk). They exist for logging/inspection only.

### 5.3 Global write protection

GORM blocks `Update`/`Delete` without a `WHERE` clause or primary key (`ErrMissingWhereClause`), preventing accidental full-table mutation. This guard is bypassed only by an explicit `AllowGlobalUpdate` session — keep that opt-in tightly scoped.

### 5.4 Read/write field permissions

Field tags `->` (read-only), `<-` (write-only/creatable), and `-` (ignored) let you keep sensitive or computed columns out of inserts/updates at the mapping layer, reducing mass-assignment risk.

### 5.5 Soft deletes and data retention

Models with `gorm.DeletedAt` are never physically removed by `Delete`; rows persist with `deleted_at` set. For data-retention/GDPR-style hard deletes, you must use `db.Unscoped().Delete(...)`.

### 5.6 Logging parameters

Enable `ParameterizedQueries: true` to keep bind values (which may be credentials, tokens, or PII) out of SQL logs.

---

## 6. Operations

GORM has no process to start, stop, or monitor. "Operations" means managing the database connection it owns and observing query behavior.

### 6.1 Connection pool management

The pool is the standard library's. Obtain it with `db.DB()` and tune:

```go
sqlDB, err := db.DB()
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(30 * time.Minute)
```

| Knob | Guidance |
|------|----------|
| `SetMaxOpenConns` | Cap to what your database can handle; too high causes contention server-side. |
| `SetMaxIdleConns` | Keep ≥ your steady-state concurrency to avoid churn; ≤ `MaxOpenConns`. |
| `SetConnMaxLifetime` | Set below the database's/load-balancer's idle timeout to avoid "broken pipe" errors. |

### 6.2 Health checks

```go
sqlDB, _ := db.DB()
if err := sqlDB.PingContext(ctx); err != nil { /* unhealthy */ }
```

`PreparedStmtDB` and `PreparedStmtTX` also implement `Ping()`, delegating to the underlying `*sql.DB`.

### 6.3 Prepared-statement cache

With `PrepareStmt: true`, GORM wraps the pool in a `PreparedStmtDB` (`prepare_stmt.go`) backed by an LRU `stmt_store.Store` (`internal/stmt_store/`). Statements are keyed by SQL text, guarded by an `sync.RWMutex`, and evicted on `driver.ErrBadConn` or TTL expiry. Call `(*PreparedStmtDB).Close()` to clear all cached statements (e.g. on reconnect).

> [!NOTE]
> Open issue #7788 reports lock contention and a potential goroutine leak in the prepared-statement cache / LRU under high concurrency. If you run `PrepareStmt: true` at scale, monitor goroutine counts and benchmark against your workload.

### 6.4 Observability

The logger's `Trace` is the built-in observability hook: it logs every statement with elapsed time and rows affected, flags slow queries above `SlowThreshold`, and surfaces errors (other than ignored `ErrRecordNotFound`). Route it through `slog` for structured ingestion, or implement `logger.Interface` to push metrics/traces to your APM.

### 6.5 Migrations

`db.AutoMigrate(&Models...)` creates missing tables, adds missing columns and indexes, and updates column types — but **never drops** columns or tables (non-destructive). For full DDL control use the `Migrator()` interface:

```go
m := db.Migrator()
m.CreateTable(&User{})
m.HasTable(&User{})
m.AddColumn(&User{}, "Age")
m.CreateIndex(&User{}, "idx_users_name")
m.DropColumn(&User{}, "Obsolete")   // explicit, destructive — your call
```

The full `Migrator` interface (`migrator.go`) covers databases, tables, columns, views, constraints, and indexes; each driver provides a concrete implementation built on the base `migrator.Migrator` (`migrator/migrator.go`).

> [!WARNING]
> `AutoMigrate` is convenient for development and additive production changes, but it will not remove or rename columns and offers no down-migrations or version history. For destructive or auditable schema changes in production, drive DDL through the explicit `Migrator` methods (or a dedicated migration tool) inside a reviewed change process.

---

## 7. Extending GORM

### 7.1 Plugins

A plugin implements `Plugin` (`Name() string`, `Initialize(*DB) error`) and is registered with `db.Use(plugin)`. `Use` rejects duplicates by name with `ErrRegistered` and calls `Initialize` immediately. Plugins typically register or replace callbacks and/or clause builders.

```go
type MetricsPlugin struct{}
func (MetricsPlugin) Name() string { return "metrics" }
func (p MetricsPlugin) Initialize(db *gorm.DB) error {
	return db.Callback().Query().After("gorm:query").Register("metrics:query", recordQuery)
}

db.Use(MetricsPlugin{})
```

### 7.2 Custom callbacks

Insert, replace, or remove behavior anywhere in a processor's pipeline:

```go
db.Callback().Create().Before("gorm:create").Register("audit:stamp", stampActor)
db.Callback().Update().Replace("gorm:update", customUpdate)
db.Callback().Delete().Remove("gorm:before_delete")
```

### 7.3 Custom data types (`Valuer` / `Scanner`)

Any type implementing `driver.Valuer` + `sql.Scanner` works as a column. For SQL-expression-valued types, implement GORM's `Valuer` interface (`GormValue(ctx, *DB) clause.Expr`) to inject expressions like `ST_GeomFromText(?)`.

### 7.4 Serializers

The `schema.SerializerInterface` (with built-ins `JSONSerializer`, `GobSerializer`, `UnixSecondSerializer` in `schema/serializer.go`) transforms a Go field to/from its stored representation via the `serializer` tag (`gorm:"serializer:json"`).

### 7.5 Writing a dialector

To support a new database, implement the `Dialector` interface (`Name`, `Initialize`, `Migrator`, `DataTypeOf`, `DefaultValueOf`, `BindVarTo`, `QuoteTo`, `Explain`) plus a `migrator.Migrator` subtype. The existing `gorm.io/driver/*` packages are the reference implementations.

---

## 8. Repository Structure

| Path | Contents |
|------|----------|
| `gorm.go` | `DB`, `Config`, `Session`, `Open`, sessions, plugins, `ToSQL`. |
| `chainable_api.go` | Chain builders: `Model`, `Where`, `Select`, `Joins`, `Order`, `Limit`, `Preload`, … |
| `finisher_api.go` | Executors: `Create`, `First`, `Find`, `Save`, `Update(s)`, `Delete`, `Count`, `Transaction`, `Exec`, … |
| `generics.go` | The type-safe `gorm.G[T]` API and `WithResult`. |
| `association.go` | Association mode (`Append`/`Replace`/`Delete`/`Clear`/`Count`). |
| `statement.go` | The `Statement` builder and condition parsing. |
| `model.go` | The `gorm.Model` convenience struct. |
| `soft_delete.go` | `DeletedAt` and the soft-delete query/update/delete clauses. |
| `prepare_stmt.go` | `PreparedStmtDB` / `PreparedStmtTX` caches. |
| `errors.go` | Exported sentinel errors. |
| `interfaces.go` | `Dialector`, `Plugin`, `ConnPool`, `Tx`, `Valuer`, `ErrorTranslator`, … |
| `migrator.go` | `Migrator`, `ColumnType`, `Index`, `TableType` interfaces; `AutoMigrate`. |
| `scan.go` | Row-scanning logic (into structs, maps, primitives, with join nesting). |
| `callbacks/` | The default pipeline + `Create`/`Query`/`Update`/`Delete`/`Preload`/`Row`/`Raw` callbacks. |
| `clause/` | Every SQL clause type. |
| `schema/` | Model parsing: fields, relationships, indexes, constraints, naming, serializers. |
| `migrator/` | Base migrator + `ColumnType`/`Index`/`TableType` concrete types. |
| `logger/` | Logger interface, default logger, `slog` logger, SQL explainer. |
| `utils/` | Helpers (`FileWithLineNum`, string utilities). |
| `internal/lru`, `internal/stmt_store` | LRU cache and prepared-statement store. |
| `tests/` | Cross-driver integration suite (separate module; uses Docker). |

---

## 9. Testing Strategy

GORM splits tests into two scopes:

- **In-package unit tests** sit beside the code they cover (`clause/*_test.go`, `schema/*_test.go`, `logger/*_test.go`, etc.) and exercise SQL generation, schema parsing, naming, and serialization without a live database.
- **Integration tests** live in `tests/` as a **separate Go module** (`gorm.io/gorm/tests`, `go 1.24.0`) that depends on the real drivers (`gorm.io/driver/mysql`, `postgres`, `sqlite`, `sqlserver`, `gaussdb`). They run against actual databases.

Running the integration suite (from `tests/README.md`):

```bash
cd tests
docker-compose up      # provision MySQL/Postgres/SQL Server/etc.
./tests_all.sh         # run the suite across all dialects
```

> [!TIP]
> When contributing or validating a driver, run `tests/` against every supported database — behavior (quoting, returning clauses, on-conflict semantics, type mapping) legitimately differs per dialect, and the suite is where those differences are pinned down.

Lint configuration lives in `.golangci.yml` (golangci-lint v2): enabled linters include `cyclop`, `gocritic`, `gosec`, `ineffassign`, `prealloc`, `unconvert`, `unparam`, plus `gofumpt`/`goimports` formatting.

---

## 10. Performance Tuning

| Lever | Effect |
|-------|--------|
| `PrepareStmt: true` | Reuse prepared statements; biggest win for repeated query shapes. |
| `SkipDefaultTransaction: true` | Removes the implicit per-write transaction; measurable on write-heavy loops. |
| `CreateBatchSize` / `CreateInBatches` | Collapse many inserts into batched multi-row statements. |
| `FindInBatches` | Bound memory for large reads by processing in chunks. |
| `Select` / `QueryFields` | Fetch only needed columns; `QueryFields` avoids `SELECT *`. |
| `Preload` vs `Joins` | `Preload` issues separate queries (avoids row multiplication); `Joins` is one query (better for filtering). Choose per access pattern. |
| Connection pool sizing | Match `MaxOpenConns`/`MaxIdleConns` to real concurrency (Section 6.1). |

The community has filed a coordinated set of performance issues against the analyzed version — useful as a tuning/awareness checklist:

| Issue | Area |
|-------|------|
| #7786 | Hot-path overhead when logger is `Silent` (Trace closure, `time.Now`, context). |
| #7792 | Preload efficiency — O(n²) assignment, concurrent loading, batch splitting. |
| #7791 | Reflection and `fmt.Sprintf` allocations in SQL-building paths. |
| #7787 | `singleflight` for schema `Parse`; cache `ParseIndexes`. |
| #7789 | `sync.Pool` for `Statement` and scan buffers to cut per-query allocations. |
| #7788 | Lock contention / goroutine leak in the prepared-statement cache / LRU. |
| #7790 | `rows.Close()` not deferred in delete/update returning paths — connection leak on panic. |

> [!CAUTION]
> Issue #7790 (un-deferred `rows.Close()` on the returning path) and #7788 (prepared-statement cache leak) describe potential **connection/goroutine leaks** under specific failure modes. If you run at high throughput with `Returning` clauses or `PrepareStmt`, monitor open connections and goroutine counts, and confirm against your pinned GORM version whether these are resolved.

---

## 11. Known Issues & Behavioral Gotchas

Sourced from `errors.go`, the code paths, and the project's tracked issues:

- **Zero-value struct conditions/updates are skipped.** Passing a struct to `Where`/`Updates` ignores zero fields; use maps or `Select`. (The single most reported source of confusion.)
- **Global writes are blocked.** Unconditioned `Update`/`Delete` return `ErrMissingWhereClause` unless `AllowGlobalUpdate` is set.
- **Error sentinels require `TranslateError`.** `ErrDuplicatedKey`/`ErrForeignKeyViolated`/`ErrCheckConstraintViolated` only surface when `TranslateError: true` and the driver implements `ErrorTranslator`; GORM warns at startup if it doesn't.
- **`AutoMigrate` is additive only.** It never drops columns/tables and has no down-migrations.
- **Soft-deleted rows linger.** `gorm.DeletedAt` models need `Unscoped().Delete()` for true removal.
- **`Joins` by association requires a defined relationship** (issue #7745); otherwise use a raw `JOIN ... ON`.
- **`SavePoint`/`RollbackTo` need driver support** (`SavePointerDialectorInterface`); otherwise `ErrUnsupportedDriver`. Prepared statements are temporarily detached around save points because some databases (e.g. MySQL 8) don't support them with prepared statements.
- **`ColumnType.DecimalSize` can panic on partially-populated driver wrappers** under concurrent `DROP COLUMN` (issue #7764) — a driver-interaction edge case.

---

## 12. Quick Reference

### 12.1 Setup

```go
db, err := gorm.Open(driver.Open(dsn), &gorm.Config{
	PrepareStmt:    true,
	TranslateError: true,
	Logger:         logger.Default.LogMode(logger.Warn),
	NowFunc:        func() time.Time { return time.Now().UTC() },
})
```

### 12.2 Sentinel errors

`ErrRecordNotFound`, `ErrInvalidTransaction`, `ErrNotImplemented`, `ErrMissingWhereClause`, `ErrUnsupportedRelation`, `ErrPrimaryKeyRequired`, `ErrModelValueRequired`, `ErrModelAccessibleFieldsRequired`, `ErrSubQueryRequired`, `ErrInvalidData`, `ErrUnsupportedDriver`, `ErrRegistered`, `ErrInvalidField`, `ErrEmptySlice`, `ErrDryRunModeUnsupported`, `ErrInvalidDB`, `ErrInvalidValue`, `ErrInvalidValueOfLength`, `ErrPreloadNotAllowed`, `ErrDuplicatedKey`, `ErrForeignKeyViolated`, `ErrCheckConstraintViolated`.

### 12.3 Callback management

```go
db.Callback().Create().Before("gorm:create").Register("name", fn)
db.Callback().Query().After("gorm:query").Register("name", fn)
db.Callback().Update().Replace("gorm:update", fn)
db.Callback().Delete().Remove("name")
```

### 12.4 Migrator

```go
db.AutoMigrate(&A{}, &B{})
m := db.Migrator()
m.HasTable(&A{}); m.CreateTable(&A{}); m.DropTable(&A{})
m.AddColumn(&A{}, "Field"); m.HasColumn(&A{}, "Field"); m.DropColumn(&A{}, "Field")
m.CreateIndex(&A{}, "idx"); m.HasIndex(&A{}, "idx"); m.DropIndex(&A{}, "idx")
m.CreateConstraint(&A{}, "fk_x"); m.HasConstraint(&A{}, "fk_x")
```

### 12.5 Session overrides

```go
db.Session(&gorm.Session{
	NewDB:                  true,  // fresh, condition-free handle
	PrepareStmt:            true,
	SkipDefaultTransaction: true,
	FullSaveAssociations:   true,
	AllowGlobalUpdate:      true,
	DryRun:                 true,
	QueryFields:            true,
	SkipHooks:              true,
	Context:                ctx,
})
```

---

_Generated June 2026 from go-gorm/gorm at d0ee5e2. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
