database

package module
v0.0.0-...-126bbbe Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 24, 2025 License: MIT Imports: 3 Imported by: 0

README

fursy plugins/database

Database integration plugin for fursy HTTP router. Provides seamless integration with database/sql for any SQL driver (PostgreSQL, MySQL, SQLite, etc.).

Features

  • Database Middleware: Share database connection across handlers
  • Transaction Helpers: Easy transaction management with auto-commit/rollback
  • Context Integration: c.DB() for convenient database access
  • Generic SQL Support: Works with any database/sql driver
  • Zero External Dependencies: Only stdlib database/sql

Installation

go get github.com/coregx/fursy/plugins/database
go get github.com/lib/pq  # PostgreSQL driver (example)

Quick Start

package main

import (
    "database/sql"
    "github.com/coregx/fursy"
    "github.com/coregx/fursy/plugins/database"
    _ "github.com/lib/pq" // PostgreSQL driver
)

func main() {
    // Open database connection.
    sqlDB, err := sql.Open("postgres", "user=postgres dbname=mydb sslmode=disable")
    if err != nil {
        panic(err)
    }
    defer sqlDB.Close()

    // Wrap with fursy database plugin.
    db := database.NewDB(sqlDB)

    // Create router with database middleware.
    router := fursy.New()
    router.Use(database.Middleware(db))

    // Use database in handlers.
    router.GET("/users/:id", func(c *fursy.Context) error {
        retrievedDB, ok := database.GetDB(c)
        if !ok {
            return c.Problem(fursy.InternalServerError("Database not configured"))
        }

        var user User
        err := retrievedDB.QueryRow(c.Request.Context(),
            "SELECT * FROM users WHERE id = $1", c.Param("id")).
            Scan(&user.ID, &user.Name)

        if err == sql.ErrNoRows {
            return c.Problem(fursy.NotFound("User not found"))
        }
        if err != nil {
            return c.Problem(fursy.InternalServerError(err.Error()))
        }

        return c.JSON(200, user)
    })

    router.Run(":8080")
}

API Reference

DB Type
type DB struct {
    // Wraps *sql.DB with context support
}
Methods
  • NewDB(db *sql.DB) *DB - Create new DB wrapper
  • DB() *sql.DB - Get underlying *sql.DB
  • Ping(ctx context.Context) error - Verify connection
  • Close() error - Close connection
  • Exec(ctx, query, args...) - Execute query without rows
  • Query(ctx, query, args...) - Execute query returning rows
  • QueryRow(ctx, query, args...) - Execute query returning single row
  • BeginTx(ctx, opts) (*Tx, error) - Start transaction
Middleware
func Middleware(db *DB) fursy.HandlerFunc

Stores database in request context, making it available to all handlers.

Usage:

db := database.NewDB(sqlDB)
router.Use(database.Middleware(db))
GetDB Helper
func GetDB(c *fursy.Context) (*DB, bool)

Retrieves database from context.

Returns:

  • *DB, true if database is configured
  • nil, false if middleware not configured

Usage:

db, ok := database.GetDB(c)
if !ok {
    return c.Problem(fursy.InternalServerError("Database not configured"))
}

Transactions

Tx Type
type Tx struct {
    // Wraps *sql.Tx with context support
}
Methods
  • Commit() error - Commit transaction
  • Rollback() error - Rollback transaction
  • Exec(ctx, query, args...) - Execute query without rows
  • Query(ctx, query, args...) - Execute query returning rows
  • QueryRow(ctx, query, args...) - Execute query returning single row
Manual Transactions
router.POST("/transfer", func(c *fursy.Context) error {
    db, _ := database.GetDB(c)

    tx, err := db.BeginTx(c.Request.Context(), nil)
    if err != nil {
        return err
    }
    defer tx.Rollback() // Rollback if not committed

    // ... do work ...

    return tx.Commit()
})
WithTx Helper
func WithTx(ctx context.Context, db *DB, fn func(*Tx) error) error

Executes a function within a transaction. Automatically commits on success, rolls back on error.

Usage:

err := database.WithTx(c.Request.Context(), db, func(tx *database.Tx) error {
    _, err := tx.Exec(ctx, "INSERT INTO users (name) VALUES ($1)", "Alice")
    if err != nil {
        return err // Automatic rollback
    }
    _, err = tx.Exec(ctx, "INSERT INTO audit (action) VALUES ($1)", "user_created")
    return err // Automatic commit on nil error
})
Transaction Middleware
func TxMiddleware(db *DB) fursy.HandlerFunc

Wraps each request in a database transaction. Auto-commits on success, auto-rolls back on error.

Usage:

// Apply to specific routes that need transactions.
txGroup := router.Group("/api/v1")
txGroup.Use(database.Middleware(db))
txGroup.Use(database.TxMiddleware(db))

txGroup.POST("/users", func(c *fursy.Context) error {
    tx, _ := database.GetTx(c)
    // Use tx for all database operations
    // Auto-commit on success, auto-rollback on error
    return nil
})

GetTx Helper:

func GetTx(c *fursy.Context) (*Tx, bool)

Retrieves transaction from context (requires TxMiddleware).

Examples

CRUD Operations

See examples/09-rest-api-with-db for a complete REST API example with:

  • Create, Read, Update, Delete operations
  • Transaction management
  • Error handling with RFC 9457
  • Batch operations
Batch Insert with Transaction
router.POST("/users/batch", func(c *fursy.Context) error {
    db, _ := database.GetDB(c)

    var users []User
    json.NewDecoder(c.Request.Body).Decode(&users)

    var count int
    err := database.WithTx(c.Request.Context(), db, func(tx *database.Tx) error {
        for _, user := range users {
            _, err := tx.Exec(c.Request.Context(),
                "INSERT INTO users (name) VALUES ($1)", user.Name)
            if err != nil {
                return err // Rollback entire batch
            }
            count++
        }
        return nil // Commit all inserts
    })

    if err != nil {
        return c.Problem(fursy.InternalServerError(err.Error()))
    }

    return c.Created(map[string]int{"inserted": count})
})
Error Handling
router.GET("/users/:id", func(c *fursy.Context) error {
    db, _ := database.GetDB(c)

    var user User
    err := db.QueryRow(c.Request.Context(),
        "SELECT id, name FROM users WHERE id = $1", c.Param("id")).
        Scan(&user.ID, &user.Name)

    if err == sql.ErrNoRows {
        return c.Problem(fursy.NotFound("User not found"))
    }
    if err != nil {
        return c.Problem(fursy.InternalServerError(err.Error()))
    }

    return c.JSON(200, user)
})

Supported Databases

Any database/sql compatible driver:

  • PostgreSQL: github.com/lib/pq or github.com/jackc/pgx/v5/stdlib
  • MySQL: github.com/go-sql-driver/mysql
  • SQLite: github.com/mattn/go-sqlite3
  • SQL Server: github.com/denisenkom/go-mssqldb
  • Oracle: github.com/godror/godror

dbcontext Pattern

The dbcontext pattern refers to best practices for managing database connections in request context. This plugin provides three approaches with different trade-offs:

Use when: You want clean error handling with RFC 9457 Problem Details.

router.GET("/users/:id", func(c *fursy.Context) error {
    db, err := database.GetDBOrError(c)
    if err != nil {
        return err // Returns 500 Internal Server Error
    }

    var user User
    err = db.QueryRow(c.Request.Context(),
        "SELECT id, name FROM users WHERE id = $1", c.Param("id")).
        Scan(&user.ID, &user.Name)

    if err == sql.ErrNoRows {
        return c.Problem(fursy.NotFound("User not found"))
    }
    return c.JSON(200, user)
})

Pros:

  • Clean, production-ready error handling
  • Returns RFC 9457 compliant errors
  • Single line to get DB with error handling

Cons:

  • Requires error check on every handler
Approach 2: MustGetDB (For Prototyping)

Use when: Rapid prototyping or when DB absence indicates programming error.

router.GET("/users", func(c *fursy.Context) error {
    db := database.MustGetDB(c) // Panics if middleware not configured

    rows, err := db.Query(c.Request.Context(), "SELECT * FROM users")
    // ... handle query errors only
})

Pros:

  • Minimal boilerplate
  • Fast to write during development

Cons:

  • Panics on misconfiguration (not production-friendly)
  • Less explicit error handling
Approach 3: GetDB (Manual Control)

Use when: You need custom error handling or conditional DB usage.

router.GET("/users", func(c *fursy.Context) error {
    db, ok := database.GetDB(c)
    if !ok {
        // Custom error handling
        return c.Problem(fursy.Problem{
            Type:   "https://example.com/errors/db-not-configured",
            Title:  "Database Unavailable",
            Status: 503,
            Detail: "Service is temporarily unavailable",
        })
    }
    // Use db...
})

Pros:

  • Full control over error handling
  • Can return custom error responses

Cons:

  • More verbose
  • Requires manual error construction
When to Use Each Approach
Scenario Recommended Approach
Production REST API GetDBOrError() - Clean errors
Internal Admin Panel MustGetDB() - Fast prototyping
Microservice Health Check GetDB() - Custom 503 responses
Conditional DB Usage GetDB() - Check availability
Transaction Patterns

Similar helpers exist for transactions:

GetTxOrError (Recommended):

txGroup := router.Group("/api")
txGroup.Use(database.TxMiddleware(db))

txGroup.POST("/transfer", func(c *fursy.Context) error {
    tx, err := database.GetTxOrError(c)
    if err != nil {
        return err
    }
    // Use tx - auto-commit on success, auto-rollback on error
})

MustGetTx (Prototyping):

txGroup.POST("/batch", func(c *fursy.Context) error {
    tx := database.MustGetTx(c) // Panics if TxMiddleware not configured
    // Use tx...
})
Repository Pattern Integration

Combine dbcontext with repository pattern for clean separation:

// Repository encapsulates database operations
type UserRepository struct {
    db *database.DB
}

func NewUserRepository(db *database.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
    var user User
    err := r.db.QueryRow(ctx,
        "SELECT id, name FROM users WHERE id = $1", id).
        Scan(&user.ID, &user.Name)

    if err == sql.ErrNoRows {
        return nil, ErrUserNotFound
    }
    return &user, err
}

// Handler using repository pattern
router.GET("/users/:id", func(c *fursy.Context) error {
    db, err := database.GetDBOrError(c)
    if err != nil {
        return err
    }

    repo := NewUserRepository(db)
    user, err := repo.FindByID(c.Request.Context(), c.Param("id"))
    if err == ErrUserNotFound {
        return c.Problem(fursy.NotFound("User not found"))
    }
    if err != nil {
        return c.Problem(fursy.InternalServerError(err.Error()))
    }

    return c.JSON(200, user)
})

Benefits:

  • Testable (mock repository interface)
  • Clean separation of concerns
  • Reusable across handlers
  • Type-safe domain errors

Best Practices

Connection Pooling

Configure connection pool settings for production:

sqlDB, _ := sql.Open("postgres", dsn)

// Configure pool
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(5)
sqlDB.SetConnMaxLifetime(5 * time.Minute)

db := database.NewDB(sqlDB)
Use Transactions for Multi-Step Operations

Always use transactions for operations that modify multiple rows or tables:

database.WithTx(ctx, db, func(tx *database.Tx) error {
    // Step 1: Insert user
    // Step 2: Insert audit log
    // Both succeed or both fail
    return nil
})
Context Cancellation

Always pass request context to database operations:

db.QueryRow(c.Request.Context(), query, args...) // Use c.Request.Context()

This ensures:

  • Operations are canceled if client disconnects
  • Timeout policies are enforced
  • Graceful shutdown works correctly
Prepared Statements

For frequently executed queries, use prepared statements:

stmt, _ := db.DB().PrepareContext(ctx, "SELECT * FROM users WHERE id = $1")
defer stmt.Close()
// Use stmt.QueryRowContext(ctx, id)

Testing

Run tests:

cd plugins/database
go test -v ./...

Run with coverage:

go test -coverprofile=coverage.txt ./...
go tool cover -html=coverage.txt

License

MIT License - see LICENSE for details.

See Also

Database Drivers

Compatible with any database/sql driver:

  • PostgreSQL: github.com/lib/pq
  • MySQL: github.com/go-sql-driver/mysql
  • SQLite: modernc.org/sqlite (pure Go, no CGO)
  • SQL Server: github.com/denisenkom/go-mssqldb

See database/sql drivers for full list.

Documentation

Overview

Package database provides database integration middleware for fursy HTTP router.

This package provides:

  • DB wrapper for *sql.DB with context support
  • Middleware to share database connection across handlers
  • Transaction helpers with auto-commit/rollback
  • Context integration via c.DB() method

Example:

import (
    "database/sql"
    "github.com/coregx/fursy"
    "github.com/coregx/fursy/plugins/database"
    _ "github.com/lib/pq" // PostgreSQL driver
)

sqlDB, _ := sql.Open("postgres", dsn)
db := database.NewDB(sqlDB)

router := fursy.New()
router.Use(database.Middleware(db))

router.GET("/users/:id", func(c *fursy.Context) error {
    db := c.DB()
    var user User
    err := db.QueryRow(c.Request.Context(),
        "SELECT * FROM users WHERE id = $1", c.Param("id")).
        Scan(&user.ID, &user.Name)
    if err != nil {
        return c.Problem(fursy.NotFound("User not found"))
    }
    return c.JSON(200, user)
})

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Middleware

func Middleware(db *DB) fursy.HandlerFunc

Middleware creates a middleware that stores the database in the request context.

This allows handlers to access the database via c.DB() method.

Example:

db := database.NewDB(sqlDB)
router.Use(database.Middleware(db))

router.GET("/users", func(c *fursy.Context) error {
    db := c.DB()
    // Use db for queries...
    return nil
})

func TxMiddleware

func TxMiddleware(db *DB) fursy.HandlerFunc

TxMiddleware creates a middleware that wraps each request in a database transaction.

The transaction is automatically committed if the handler succeeds (returns nil), or rolled back if the handler returns an error.

This is useful for endpoints that require transactional guarantees.

Example:

// Apply to specific routes that need transactions:
txGroup := router.Group("/api/v1")
txGroup.Use(database.Middleware(db))
txGroup.Use(database.TxMiddleware(db))

txGroup.POST("/users", func(c *fursy.Context) error {
    tx, _ := database.GetTx(c)
    // Use tx for all database operations
    // Auto-commit on success, auto-rollback on error
    return nil
})

func WithTx

func WithTx(ctx context.Context, db *DB, fn func(*Tx) error) error

WithTx executes a function within a database transaction.

If the function returns an error, the transaction is rolled back. Otherwise, the transaction is committed.

This is a convenience helper that handles transaction lifecycle automatically.

Example:

err := database.WithTx(ctx, db, func(tx *database.Tx) error {
    _, err := tx.Exec(ctx, "INSERT INTO users (name) VALUES ($1)", "Bob")
    if err != nil {
        return err // Automatic rollback
    }
    _, err = tx.Exec(ctx, "INSERT INTO audit (action) VALUES ($1)", "user_created")
    return err // Automatic commit on nil error
})

Types

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB wraps a *sql.DB connection with context support.

It provides a thin wrapper around database/sql that integrates with fursy's context and middleware system.

func GetDB

func GetDB(c *fursy.Context) (*DB, bool)

GetDB retrieves the database from the context.

Returns (nil, false) if database middleware is not configured.

Example:

db, ok := database.GetDB(c)
if !ok {
    return c.Problem(fursy.InternalServerError("Database not configured"))
}

func GetDBOrError

func GetDBOrError(c *fursy.Context) (*DB, error)

GetDBOrError retrieves the database from the context or returns an RFC 9457 error.

This is a convenience helper that combines GetDB() with error handling. Returns InternalServerError (500) if database middleware is not configured.

This is the recommended approach for production APIs where database misconfiguration should return a proper error response.

Example:

router.GET("/users", func(c *fursy.Context) error {
    db, err := database.GetDBOrError(c)
    if err != nil {
        return c.Problem(err.(fursy.Problem))
    }
    rows, err := db.Query(c.Request.Context(), "SELECT * FROM users")
    // ...
    return c.JSON(200, users)
})

func MustGetDB

func MustGetDB(c *fursy.Context) *DB

MustGetDB retrieves the database from the context or panics.

This is a convenience helper for handlers where database is required. Panics with a descriptive message if database middleware is not configured.

Use this in handlers where database absence indicates a programming error (i.e., middleware misconfiguration), not a runtime error.

Example:

router.GET("/users", func(c *fursy.Context) error {
    db := database.MustGetDB(c) // Panic if DB not configured
    rows, err := db.Query(c.Request.Context(), "SELECT * FROM users")
    // ...
    return c.JSON(200, users)
})

For production APIs with proper error handling, use GetDBOrError() instead.

func NewDB

func NewDB(db *sql.DB) *DB

NewDB creates a new DB wrapper around a *sql.DB connection.

Example:

sqlDB, err := sql.Open("postgres", dsn)
if err != nil {
    log.Fatal(err)
}
db := database.NewDB(sqlDB)

func (*DB) BeginTx

func (d *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)

BeginTx starts a new database transaction.

The provided context is used until the transaction is committed or rolled back. If the context is canceled, the sql package will roll back the transaction.

Example:

tx, err := db.BeginTx(ctx, nil)
if err != nil {
    return err
}
defer tx.Rollback() // Rollback if not committed

// ... perform operations ...

return tx.Commit()

func (*DB) Close

func (d *DB) Close() error

Close closes the database connection.

It is rare to Close a DB, as the DB handle is meant to be long-lived and shared between many goroutines.

func (*DB) DB

func (d *DB) DB() *sql.DB

DB returns the underlying *sql.DB connection.

This is useful when you need to access the raw database/sql API.

Example:

stats := db.DB().Stats()

func (*DB) Exec

func (d *DB) Exec(ctx context.Context, query string, args ...any) (sql.Result, error)

Exec executes a query without returning rows.

Example:

result, err := db.Exec(ctx, "DELETE FROM users WHERE id = $1", userID)
if err != nil {
    return err
}
rowsAffected, _ := result.RowsAffected()

func (*DB) Ping

func (d *DB) Ping(ctx context.Context) error

Ping verifies a connection to the database is still alive.

Example:

if err := db.Ping(ctx); err != nil {
    log.Fatal("Database connection lost:", err)
}

func (*DB) Query

func (d *DB) Query(ctx context.Context, query string, args ...any) (*sql.Rows, error)

Query executes a query that returns rows.

Example:

rows, err := db.Query(ctx, "SELECT id, name FROM users")
if err != nil {
    return err
}
defer rows.Close()

for rows.Next() {
    var id int
    var name string
    rows.Scan(&id, &name)
}

func (*DB) QueryRow

func (d *DB) QueryRow(ctx context.Context, query string, args ...any) *sql.Row

QueryRow executes a query that is expected to return at most one row.

Example:

var user User
err := db.QueryRow(ctx, "SELECT id, name FROM users WHERE id = $1", userID).
    Scan(&user.ID, &user.Name)
if err == sql.ErrNoRows {
    return ErrNotFound
}

type Tx

type Tx struct {
	// contains filtered or unexported fields
}

Tx wraps a *sql.Tx transaction with context support.

Transactions provide ACID guarantees for database operations. All operations within a transaction are atomic - they either all succeed (commit) or all fail (rollback).

func GetTx

func GetTx(c *fursy.Context) (*Tx, bool)

GetTx retrieves the transaction from the context.

Returns (nil, false) if TxMiddleware is not configured for this request.

Example:

tx, ok := database.GetTx(c)
if !ok {
    return c.Problem(fursy.InternalServerError("Transaction not available"))
}
_, err := tx.Exec(c.Request.Context(), "INSERT INTO ...")

func GetTxOrError

func GetTxOrError(c *fursy.Context) (*Tx, error)

GetTxOrError retrieves the transaction from the context or returns an RFC 9457 error.

This is a convenience helper that combines GetTx() with error handling. Returns InternalServerError (500) if TxMiddleware is not configured.

This is the recommended approach for production APIs where transaction unavailability should return a proper error response.

Example:

txGroup.POST("/transfer", func(c *fursy.Context) error {
    tx, err := database.GetTxOrError(c)
    if err != nil {
        return c.Problem(err.(fursy.Problem))
    }
    _, err = tx.Exec(c.Request.Context(), "UPDATE accounts SET ...")
    return err
})

func MustGetTx

func MustGetTx(c *fursy.Context) *Tx

MustGetTx retrieves the transaction from the context or panics.

This is a convenience helper for handlers where transaction is required. Panics with a descriptive message if TxMiddleware is not configured.

Use this in handlers where transaction absence indicates a programming error (i.e., middleware misconfiguration), not a runtime error.

Example:

txGroup := router.Group("/api")
txGroup.Use(database.TxMiddleware(db))

txGroup.POST("/transfer", func(c *fursy.Context) error {
    tx := database.MustGetTx(c) // Panic if TxMiddleware not configured
    _, err := tx.Exec(c.Request.Context(), "UPDATE accounts SET ...")
    return err
})

For production APIs with proper error handling, use GetTxOrError() instead.

func (*Tx) Commit

func (t *Tx) Commit() error

Commit commits the transaction.

Returns an error if the transaction has already been committed or rolled back.

func (*Tx) Exec

func (t *Tx) Exec(ctx context.Context, query string, args ...any) (sql.Result, error)

Exec executes a query without returning rows within the transaction.

Example:

_, err := tx.Exec(ctx, "INSERT INTO users (name) VALUES ($1)", "Alice")

func (*Tx) Query

func (t *Tx) Query(ctx context.Context, query string, args ...any) (*sql.Rows, error)

Query executes a query that returns rows within the transaction.

Example:

rows, err := tx.Query(ctx, "SELECT id, name FROM users")
if err != nil {
    return err
}
defer rows.Close()

func (*Tx) QueryRow

func (t *Tx) QueryRow(ctx context.Context, query string, args ...any) *sql.Row

QueryRow executes a query that returns at most one row within the transaction.

Example:

var name string
err := tx.QueryRow(ctx, "SELECT name FROM users WHERE id = $1", userID).Scan(&name)

func (*Tx) Rollback

func (t *Tx) Rollback() error

Rollback aborts the transaction.

Rollback is safe to call even if the transaction has already been committed. In that case, it returns sql.ErrTxDone.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL