<!--
  Database Integration
  From the Iris book, chapter 13, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/database-integration/
-->
# Chapter 13: Database Integration

Iris is database-agnostic by design: it ships no ORM, no drivers, and no
storage adapters. What it does ship is everything a database-backed
application needs around the driver: dependency injection to construct and
share connections, lifecycle hooks for startup checks and graceful
shutdown, request contexts that cancel queries when clients disconnect,
and a central error map that turns driver errors into correct HTTP
responses. This chapter teaches that pattern once, shows it end to end
with PostgreSQL, and tells you how to apply it to any other store.

## Background

A **database** is a program whose entire job is storing and retrieving
data reliably. Relational databases (PostgreSQL, MySQL, SQLite) organize
data into **tables**: grids where each **row** is one record (one
product, one user) and each column one attribute. Document databases
(MongoDB) store **documents** instead, JSON-like records that need not
share a fixed shape. You could, in principle, persist data by writing
files yourself, until two requests write at the same moment, the
process crashes mid-write, or you need "all products cheaper than X"
without reading every file. Databases exist because they solve exactly
those problems: concurrent access, crash safety and fast lookup over
large data sets.

Four terms carry this chapter:

- A **query** is a request you send to the database ("give me the
  product with this ID", "insert this row"), usually written in SQL,
  the standard language of relational databases.
- A **connection** is the channel a query travels over. Opening one is
  comparatively expensive, so applications keep a **connection pool**:
  a small set of open connections that requests borrow and return,
  instead of every request dialing the database from scratch.
- A **transaction** groups several statements into an all-or-nothing
  unit: either every step commits, or the database rolls them all back
  as if none had happened. Money leaving one account must arrive in
  another; a transaction is how the database promises there is no
  in-between state.
- An **ORM-style layer** (object-relational mapper) translates between
  your language's structs and the database's rows so you do not
  hand-write the copying in either direction. In this chapter that role
  is played by `pg.Repository[T]`, which scans rows straight into your
  `Product` struct.

Everything in this chapter flows through one Go **interface**: a named
set of method signatures that any type can satisfy. Handlers and
services depend on that interface, never on a concrete driver, which is
what buys the chapter's repeated promise: the real database, an
in-memory fake for tests and any future replacement are interchangeable
implementations behind the same four methods. What disappears from your
code is storage detail spread across handlers (connection handles,
driver error checks, SQL strings next to HTTP status codes), leaving one
file per database that you can swap with one line of wiring.

## The Repository Seam

Handlers never talk to a database. They call a service; the service talks
to storage through an interface. That interface, the repository seam,
is the single point where the application meets persistence, and it is
what makes the rest of this chapter mechanical: to change databases you
write one new type and change one line of wiring.

\[Diagram: The repository interface is the seam where the application meets persistence: services speak domain sentinels, only a store knows a driver error, and one error map turns those sentinels into status codes.]

The running example is a small product catalog. First the domain:

```go
// catalog/catalog.go
package catalog

import (
    "errors"
    "time"

    "github.com/kataras/iris/v14/rest/errors/validation"
)

// Sentinel errors: the domain speaks its own language. The mapping to
// HTTP status codes happens once, in main.go (see the Errors section).
var (
    ErrNotFound = errors.New("product not found")
    ErrSKUTaken = errors.New("sku already exists")
)

// Product is the stored entity. The pg struct tags describe the
// PostgreSQL columns for the store you will meet later in this chapter;
// every other store (and the JSON encoder) ignores them.
type Product struct {
    ID        string    `pg:"type=uuid,primary,default=gen_random_uuid()" json:"id"`
    CreatedAt time.Time `pg:"type=timestamp,default=clock_timestamp()" json:"created_at"`
    Name      string    `pg:"type=varchar(255)" json:"name"`
    SKU       string    `pg:"type=varchar(64),unique" json:"sku"`
    Price     int64     `pg:"type=bigint" json:"price"` // Cents.
}

// ProductInput is the request payload. ctx.BindJSON runs Validate
// automatically after decoding, so handlers never call it by hand.
type ProductInput struct {
    Name  string `json:"name"`
    SKU   string `json:"sku"`
    Price int64  `json:"price"`
}

func (in ProductInput) Validate() error {
    return validation.Join(
        validation.String("name", in.Name).NotEmpty().Length(1, 255),
        validation.String("sku", in.SKU).NotEmpty().Length(1, 64),
        validation.Number("price", in.Price).Positive(),
    )
}
```

If you dislike driver tags on a domain struct, define a separate storage
entity inside the store implementation and map between the two. For most
applications the single struct is the right trade: one type, one JSON
shape, one schema definition.

The seam itself:

```go
// catalog/repository.go
package catalog

import "context"

// Repository is the storage seam: the service depends on this
// interface, production wires a database implementation, tests wire
// MemStore or a one-method stub. Every method takes a context.Context
// first — iris.Context implements it, so request cancellation reaches
// the database driver with zero extra code.
type Repository interface {
    Create(ctx context.Context, p Product) (id string, err error) // ErrSKUTaken
    List(ctx context.Context) ([]Product, error)
    Get(ctx context.Context, id string) (Product, error) // ErrNotFound
    Delete(ctx context.Context, id string) (bool, error)
}
```

Two details are load-bearing. The first parameter of every method is a
plain `context.Context`: handlers pass their `iris.Context` straight
through, and when a client disconnects, the cancellation propagates into
`pgx`, `database/sql`, the MongoDB driver (any context-aware driver)
and aborts the query. (When a write must outlive the request, an audit
insert after the response for example, pass `ctx.WithoutCancel()`
instead: it detaches from cancellation but keeps request values.) The
second detail is that ID generation lives inside the store, not the
service: an in-memory store mints UUIDs itself, PostgreSQL uses a
`gen_random_uuid()` column default, and the seam hides the difference by
returning the new id.

The in-memory implementation is ordinary Go: a mutex and a map. Iris
serves each request on its own **goroutine** (Go's lightweight thread),
so two requests can touch this map at the same moment; the
`sync.RWMutex` is the guard that prevents corruption, giving writers
exclusive access while readers share it. This store is the development
store and, later, the test suite's fake:

```go
// catalog/memstore.go
package catalog

import (
    "context"
    "sort"
    "sync"
    "time"
    "uuid"
)

type MemStore struct {
    mu       sync.RWMutex
    products map[string]Product
}

var _ Repository = (*MemStore)(nil)

func NewMemStore() *MemStore {
    return &MemStore{products: make(map[string]Product)}
}

func (s *MemStore) Create(_ context.Context, p Product) (string, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    for _, existing := range s.products {
        if existing.SKU == p.SKU {
            return "", ErrSKUTaken
        }
    }

    p.ID = uuid.New().String()
    p.CreatedAt = time.Now().UTC()
    s.products[p.ID] = p
    return p.ID, nil
}

func (s *MemStore) List(_ context.Context) ([]Product, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    list := make([]Product, 0, len(s.products))
    for _, p := range s.products {
        list = append(list, p)
    }
    sort.Slice(list, func(i, j int) bool {
        return list[i].CreatedAt.Before(list[j].CreatedAt)
    })
    return list, nil
}

func (s *MemStore) Get(_ context.Context, id string) (Product, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    p, ok := s.products[id]
    if !ok {
        return Product{}, ErrNotFound
    }
    return p, nil
}

func (s *MemStore) Delete(_ context.Context, id string) (bool, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    if _, ok := s.products[id]; !ok {
        return false, nil
    }
    delete(s.products, id)
    return true, nil
}
```

Business rules live in a service that depends on the seam. Here it is
thin; in a real application this is where invariants, authorization
checks and cross-entity logic accumulate (see
[Chapter 8](https://iris-go.com/docs/dependency-injection/) for the layering rationale):

```go
// catalog/service.go
package catalog

import "context"

// Service holds the business rules. Its constructor declares what it
// needs; the iris dependency container calls it with the registered
// Repository.
type Service struct {
    repo Repository
}

func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

func (s *Service) Create(ctx context.Context, in ProductInput) (string, error) {
    return s.repo.Create(ctx, Product{
        Name:  in.Name,
        SKU:   in.SKU,
        Price: in.Price,
    })
}

func (s *Service) List(ctx context.Context) ([]Product, error) {
    return s.repo.List(ctx)
}

func (s *Service) Get(ctx context.Context, id string) (Product, error) {
    return s.repo.Get(ctx, id)
}

func (s *Service) Delete(ctx context.Context, id string) (bool, error) {
    return s.repo.Delete(ctx, id)
}
```

## Wiring a Store Through Services

The HTTP layer stays thin: read input, call the service, hand the result
and the error to a response helper. Note that every service call receives
`ctx` (the `iris.Context`) as its `context.Context`:

```go
// api/products.go
package api

import (
    "github.com/kataras/iris/v14"
    "shop/catalog"
)

type ProductsAPI struct {
    svc *catalog.Service
}

func NewProductsAPI(svc *catalog.Service) *ProductsAPI {
    return &ProductsAPI{svc: svc}
}

func (api *ProductsAPI) Routing(r iris.Router) {
    r.Get("/", api.list)
    r.Post("/", api.create)
    r.Get("/{id:uuid}", api.get)
    r.Delete("/{id:uuid}", api.delete)
}

func (api *ProductsAPI) list(ctx iris.Context) {
    products, err := api.svc.List(ctx)
    ctx.List(products, err)
}

func (api *ProductsAPI) create(ctx iris.Context) {
    in, ok := ctx.BindJSON[catalog.ProductInput]()
    if !ok {
        return // 400 already written: malformed JSON or failed Validate.
    }

    id, err := api.svc.Create(ctx, in)
    ctx.Created(id, err) // 201 with {"id": "..."}.
}

func (api *ProductsAPI) get(ctx iris.Context) {
    p, err := api.svc.Get(ctx, ctx.Params().GetString("id"))
    ctx.OK(p, err) // ErrNotFound is mapped to 404 centrally.
}

func (api *ProductsAPI) delete(ctx iris.Context) {
    deleted, err := api.svc.Delete(ctx, ctx.Params().GetString("id"))
    ctx.NoContent(deleted, err) // 204 deleted, 304 not found.
}
```

These four handlers exist because a `catalog.Service` sits between them
and the store, and that service owns the SKU invariant. When there is no
such rule and the handlers would only forward to the repository, do not
write them: `mvc.UseRepository(repo)` registers the same resource from
the repository alone, with `mvc.Guard` for the write routes. See
[Chapter 24](https://iris-go.com/docs/advanced-topics/).

Everything assembles in the builder. The repository arrives as a
parameter so `main` can pass the real store while tests pass fakes:

```go
// main.go
package main

import (
    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/rest/errors"

    "shop/api"
    "shop/catalog"
)

func newApp(repo catalog.Repository) *iris.Application {
    return iris.NewBuilder(iris.DefaultOptions).
        Prefix("/api").
        AllowOrigin("none").
        Compression(true).
        LogRequests(false).
        Health(true, "development", "shop").
        Errors(errors.NewOptions().
            MapErrors(errors.NotFound, catalog.ErrNotFound).
            MapErrors(errors.AlreadyExists, catalog.ErrSKUTaken)).
        Services(repo, catalog.NewService).
        RouterMiddlewares().
        Middlewares().
        API("/products", api.NewProductsAPI).
        Build()
}

func main() {
    app := newApp(catalog.NewMemStore())
    app.Listen(":8080")
}
```

`Services` does more than store values for injection: it routes them by
shape, and two of those rules exist precisely for database stores:

- A registered value (or constructor output) with a `Close()` or
  `Close() error` method becomes a **shutdown closer** automatically: it
  runs when the application shuts down gracefully, after in-flight
  requests drain. No manual `defer db.Close()` bookkeeping.
- A service implementing `iris.Initializer` (`Init(ctx context.Context)
  error`) runs exactly once inside `Build()`, after dependency
  injection and before the server starts. This is the place for schema
  creation, a connectivity ping or cache warm-up; a returned error
  aborts startup instead of failing on the first request. It is the
  startup counterpart of `Deferrables`.

The next section uses both.

## PostgreSQL with kataras/pg

Two more terms before the code. A database **schema** is the set of
table definitions (names, column types, rules) that must exist before
the first query can run. A **constraint** is such a rule enforced by the
database itself: the `unique` tag on `SKU` means the database refuses a
second row with the same SKU, no matter which program inserts it, and
reports the attempt as a duplicate-key error.

For PostgreSQL, [`github.com/kataras/pg`](https://github.com/kataras/pg)
is a separate module by the Iris author, the same library Iris' own
`auth/identity` sub-package uses internally. It sits on top of the
[pgx](https://github.com/jackc/pgx) pool and derives the schema from the
`pg` struct tags you saw on `Product`: `pg.NewSchema()` registers
entities, `DB.CreateSchema` issues the corresponding
`CREATE TABLE IF NOT EXISTS` statements, and the generic
`pg.Repository[T]` scans rows into `T` without hand-written `Scan` calls.

```sh
go get github.com/kataras/pg@latest
```

The store implements the same `Repository` seam. It also implements
`Init` and `Close`, so registering it wires schema creation into
`Build()` and pool shutdown into graceful termination:

```go
// catalog/pgstore.go
package catalog

import (
    "context"
    "errors"

    "github.com/kataras/pg"
)

type PostgresStore struct {
    products *pg.Repository[Product]
}

var _ Repository = (*PostgresStore)(nil)

func NewPostgresStore(db *pg.DB) *PostgresStore {
    return &PostgresStore{products: pg.NewRepository[Product](db)}
}

// Init implements iris.Initializer: it runs once inside Build(), after
// injection, and a non-nil error aborts startup.
func (s *PostgresStore) Init(ctx context.Context) error {
    return s.products.DB().CreateSchema(ctx)
}

// Close is picked up by Services automatically and runs on graceful
// shutdown, closing the underlying pgx pool.
func (s *PostgresStore) Close() {
    s.products.DB().Close()
}

func (s *PostgresStore) Create(ctx context.Context, p Product) (string, error) {
    var id string
    if err := s.products.InsertSingle(ctx, p, &id); err != nil {
        if _, ok := pg.IsErrDuplicate(err); ok {
            return "", ErrSKUTaken // The unique "sku" constraint.
        }
        return "", err
    }
    return id, nil
}

func (s *PostgresStore) List(ctx context.Context) ([]Product, error) {
    return s.products.Select(ctx,
        `SELECT * FROM products ORDER BY created_at`)
}

func (s *PostgresStore) Get(ctx context.Context, id string) (Product, error) {
    p, err := s.products.SelectByID(ctx, id)
    if err != nil {
        if errors.Is(err, pg.ErrNoRows) {
            return Product{}, ErrNotFound
        }
        return Product{}, err
    }
    return p, nil
}

func (s *PostgresStore) Delete(ctx context.Context, id string) (bool, error) {
    return s.products.DeleteByID(ctx, id)
}
```

Notice the error translation at the boundary: `pg.ErrNoRows` becomes the
domain's `ErrNotFound` and a duplicate-key violation (detected with
`pg.IsErrDuplicate`) becomes `ErrSKUTaken`. The service and handlers
never see driver errors. The central map in `newApp` already turns
those domain errors into 404 and 409.

Swapping the database is now the promised one-line change in `main`:

```go
// main.go (PostgreSQL variant)
package main

import (
    "context"
    "log"
    "os"

    "github.com/kataras/pg"

    // Optional safety net: maps any pg driver error that escapes the
    // store untranslated (pg.ErrNoRows -> 404, duplicate key -> 409,
    // invalid input syntax -> 400). See the error mapping section.
    _ "github.com/kataras/iris/v14/rest/errors/pgerrors"

    "shop/catalog"
)

func main() {
    schema := pg.NewSchema()
    schema.MustRegister("products", catalog.Product{})

    db, err := pg.Open(context.Background(), schema,
        os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }

    app := newApp(catalog.NewPostgresStore(db))
    app.Listen(":8080")
}
```

`newApp` is unchanged. A working `DATABASE_URL` looks like:

```sh
DATABASE_URL="postgres://postgres:secret@localhost:5432/shop?sslmode=disable"
```

Pool sizing travels in the same connection string through the standard
pgx parameters (`pool_max_conns`, `pool_min_conns`,
`pool_max_conn_lifetime`, `pool_max_conn_idle_time`,
`pool_health_check_period`), so operations can tune the pool per
environment without a code change. Never hard-code the connection
string: it is a credential, and the environment (or a secret manager
feeding it) is its only home ([Chapter 11](https://iris-go.com/docs/configuration-management/)).

Beyond what the store above uses, `pg.Repository[T]` covers the rest of
day-to-day SQL with the same shape (`Insert`, `Update`,
`UpdateOnlyColumns`, `Upsert`, `Exists`, `SelectSingle` and raw
`Query`/`QueryRow` escapes) plus `InTransaction`, which the
transactions section below builds on.

## The Standard Library: database/sql

If you prefer to stay on the standard library (or you target MySQL,
SQLite or SQL Server), the seam pattern is identical; you just write the
scanning yourself. `database/sql` is stdlib; only the driver you register
(pgx's stdlib adapter, `go-sql-driver/mysql`, `modernc.org/sqlite`, ...)
is a third-party choice.

```go
// catalog/sqlstore.go
package catalog

import (
    "context"
    "database/sql"
    "errors"
)

type SQLStore struct {
    db *sql.DB
}

func NewSQLStore(db *sql.DB) *SQLStore {
    return &SQLStore{db: db}
}

func (s *SQLStore) Get(ctx context.Context, id string) (Product, error) {
    var p Product
    err := s.db.QueryRowContext(ctx,
        `SELECT id, created_at, name, sku, price
         FROM products WHERE id = $1`, id).
        Scan(&p.ID, &p.CreatedAt, &p.Name, &p.SKU, &p.Price)
    if errors.Is(err, sql.ErrNoRows) {
        return Product{}, ErrNotFound
    }
    return p, err
}
```

The remaining methods follow the same shape: `ExecContext` for writes,
`QueryContext` plus a scan loop for lists, always parameterized queries
(`$1`, `?`), never string concatenation, and always the `...Context`
variants so request cancellation reaches the database.

The parameterization rule guards against **SQL injection**, the classic
attack in which user input pasted into a query string becomes part of
the query itself: a "name" like `'; DROP TABLE products;--` rewrites
your statement. Placeholders send the query and its values to the
database separately, so input can never change the query's structure.

`*sql.DB` is itself a connection pool. Configure it once, right after
`sql.Open`, and register the store the same way as before (give it
`Close` and, if you want a startup ping, an `Init` that calls
`PingContext`):

```go
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(30 * time.Minute)
db.SetConnMaxIdleTime(5 * time.Minute)
```

The defaults (unlimited open connections, two idle) are rarely what a
production service wants, but the right numbers depend on your database
and workload. Measure before you tune.

One convenience specific to `database/sql`: the central error map
already knows `sql.ErrNoRows` and renders it as 404 by default, so even
an untranslated leak produces a correct response instead of a 500.

## MongoDB, Redis and ORMs

The seam does not care what is behind it, so third-party stores follow
the same recipe. Consult each library's own documentation for its API;
this book does not duplicate it.

**MongoDB.** Implement `Repository` with the official Go driver
(`go.mongodb.org/mongo-driver`), holding a collection handle in the
store. Pass the request context into every driver call and translate the
driver's "no documents" sentinel into `ErrNotFound` at the store
boundary, exactly as the PostgreSQL store translates `pg.ErrNoRows`.

**Redis.** Redis is usually not the repository itself but a layer beside
it: caching, rate limiting, session-like state. Keep the client
(`github.com/redis/go-redis/v9` is the common choice) registered in
`Services` so its lifecycle is managed, and see
[Chapter 14](https://iris-go.com/docs/caching-strategies/) for the cache-aside pattern that
wraps a `Repository` without the service ever knowing.

**ORMs and generators.** GORM, ent and sqlc all work with Iris for the
same reason everything else does: they live entirely inside your store
implementation. Map the library's record-not-found error into your
domain sentinel at the boundary, keep models out of your handlers, and
register the handle through `Services`. If an ORM error can escape to a
handler, add it to the central map with `MapErrors` alongside your
domain errors.

## Column Types That Survive JSON

A model crosses two boundaries with different vocabularies. SQL has
`NULL`; JSON does not have `NaN`. `rest/types` holds the small scalar
types that sit on both sides of that gap, each implementing
`database/sql`'s `Scanner` and `Valuer` as well as the JSON encoders, so
one field serves the column and the wire.

Two of them are worth naming here because they solve failures that only
show up in production data.

**`types.Number`** is a `float64` with a JSON null. `encoding/json`
refuses to encode `NaN` or an infinity, because JSON has no literal for
them, and a computed average over an empty set produces one often enough
that a single row takes the whole document down with it. `Number`
marshals all three as `null`, and a JSON `null` unmarshals back to `NaN`:

```go
type Reading struct {
    Sensor  string       `json:"sensor"`
    Average types.Number `json:"average"` // null when there is nothing to average.
}
```

The same rule holds on the database side: a `NULL` column scans to `NaN`
and a `NaN` is written as `NULL`, so a nullable `float8` needs no
`sql.NullFloat64` wrapper. The zero value is the number zero and not null;
`types.NullNumber()` returns the null one and `IsNull()` reports it.

**`types.Weekday`** is a `time.Weekday` that travels as a lowercase name:

```go
type Shift struct {
    Day types.Weekday `json:"day"` // "monday", not 1.
}
```

A client reading a schedule gets a word it can print instead of a number
whose base it has to guess, and the JSON stays readable in a log. The
column is text. `types.ParseWeekday` accepts any case and the three-letter
form (`"Monday"`, `"monday"`, `"MON"`, `"mon"`), the seven days are
exported as constants, and the two conversions are `types.WeekdayOf` going
in and `d.Std()` coming out. The numbering is `time.Weekday`'s, Sunday
being zero, which means the zero value marshals as `"sunday"` rather than
as an error: a schedule struct nobody filled in claims Sunday, so set the
field deliberately.

## Transactions Below the Handler

Transactions belong in the store (or, for multi-repository
coordination, the service), never in handlers. A handler that begins a
transaction couples HTTP concerns to storage concerns, risks holding a
database transaction open across response rendering and slow clients,
and bypasses the guarantee that every unit of work is atomic no matter
who calls it. Express each atomic unit as one method on the layer that
owns it, and let the error it returns flow through the usual pipeline.

With `kataras/pg`, `InTransaction` wraps a function in
begin/commit/rollback: return nil to commit, return an error to roll
back. The callback receives a transactional repository, so the body
reads exactly like non-transactional code:

```go
// Reprice atomically loads a product and writes the new price.
func (s *PostgresStore) Reprice(ctx context.Context, id string,
    price int64) error {
    return s.products.InTransaction(ctx,
        func(tx *pg.Repository[Product]) error {
            p, err := tx.SelectByID(ctx, id)
            if err != nil {
                if errors.Is(err, pg.ErrNoRows) {
                    return ErrNotFound // Rolls back, maps to 404.
                }
                return err
            }

            p.Price = price
            _, err = tx.Update(ctx, p)
            return err // nil commits; non-nil rolls back.
        })
}
```

`InTransaction` is re-entrant: when the repository is already inside a
transaction, the callback runs directly on the current one instead of
opening a nested transaction. That makes store methods composable: a
service can call `Reprice` on its own, or inside a larger
`InTransaction` block that spans several operations, and atomicity
follows the outermost boundary.

The same discipline with `database/sql` is a few more lines, but the
shape is identical: one store method, one atomic unit:

```go
func (s *SQLStore) Reprice(ctx context.Context, id string,
    price int64) (err error) {
    tx, err := s.db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer func() {
        if err != nil {
            tx.Rollback()
            return
        }
        err = tx.Commit()
    }()

    // All statements go through tx, with the request context.
    _, err = tx.ExecContext(ctx,
        `UPDATE products SET price = $1 WHERE id = $2`, price, id)
    return err
}
```

## Mapping Database Errors to HTTP

Everything storage-related converges on the central error map from
[Chapter 9](https://iris-go.com/docs/error-handling/). The strategy has three rings:

**Ring 1: domain errors, mapped once.** Stores translate driver errors
into domain sentinels (`ErrNotFound`, `ErrSKUTaken`), and the builder's
`Errors` step maps those to canonical codes for every handler at once:

```go
Errors(errors.NewOptions().
    MapErrors(errors.NotFound, catalog.ErrNotFound).
    MapErrors(errors.AlreadyExists, catalog.ErrSKUTaken)).
```

`ctx.OK`, `ctx.Created` and `ctx.NoContent` all hand a non-nil error
to this pipeline before rendering anything, so the handlers you wrote
earlier get correct 404 and 409 responses without a single status-code
check.

**Ring 2: driver defaults.** `errors.NewOptions()` starts from a
default map that already includes `database/sql.ErrNoRows` → 404. For
`kataras/pg` users the equivalent is opt-in, one blank import:

```go
import _ "github.com/kataras/iris/v14/rest/errors/pgerrors"
```

It registers a resolver that maps `pg.ErrNoRows` to 404, duplicate-key
violations to 409, and both PostgreSQL "invalid input syntax" and
"operator does not exist" errors to 400 with the `SQLSTATE` noise trimmed. Duplicate-key details (table, column,
constraint names, the offending value) are logged server-side but never
forwarded to the client: driver errors leak schema information, which
is exactly why this mapping exists. The import is deliberately separate
so applications that do not use PostgreSQL keep the driver out of their
dependency graph.

**Ring 3: call-site detail.** When a handler or service needs to
enrich an error before it enters the pipeline, wrap it at the call site;
the mapping still applies to the underlying error:

```go
if err := api.svc.Archive(ctx, id); err != nil {
    errors.HandleErrorf(ctx, err, "archive product %s", id)
    return
}
```

For transformations that need request data (attaching the
authenticated user to a constraint violation, for instance), register a
context-aware mapper with `MapErrorFuncCtx` on the error code map;
Chapter 9 covers it in depth.

The result is a strict layering: drivers speak driver errors, stores
speak domain errors, handlers speak `(value, err)`, and exactly one
place in the program knows that "product not found" is a 404.

## Summary

- Iris integrates with any database through a pattern, not adapters:
  handlers call a service, the service depends on a **repository
  interface**, and each database is one implementation of that seam.
- Every repository method takes `context.Context` first. `iris.Context`
  implements it, so client disconnects cancel in-flight queries; use
  `ctx.WithoutCancel()` for writes that must outlive the request.
- Register stores through the builder's `Services` step: a `Close`
  method auto-registers graceful shutdown, and `iris.Initializer`'s
  `Init` runs schema creation or a connectivity ping inside `Build()`.
- For PostgreSQL, `github.com/kataras/pg` pairs naturally with Iris:
  schema from struct tags, generic `pg.Repository[T]`, re-entrant
  `InTransaction`, and the `rest/errors/pgerrors` import for automatic
  404/409/400 mapping of driver errors.
- `database/sql` follows the identical seam with hand-written scanning;
  its `ErrNoRows` maps to 404 by default. MongoDB, Redis and ORMs
  slot in the same way: translate their sentinels at the store
  boundary.
- Transactions live in store or service methods as single atomic units,
  never in handlers; errors they return flow through the same central
  map as everything else.

## Further Reading

- [Accessing relational databases](https://go.dev/doc/database/). The
  official Go guide to `database/sql`: connections, pooling, queries and
  transactions.
- [Tutorial: Accessing a relational database](https://go.dev/doc/tutorial/database-access):
  a hands-on Go walkthrough from empty project to working queries.
- [kataras/pg package documentation](https://pkg.go.dev/github.com/kataras/pg):
  the full API of the PostgreSQL layer used in this chapter.
- [pgx package documentation](https://pkg.go.dev/github.com/jackc/pgx/v5):
  the underlying PostgreSQL driver and pool, including the connection
  string parameters.
- [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html):
  why parameterized queries are non-negotiable, with examples.
