Documentation
¶
Overview ¶
Package dbkit provides a consistent database layer for Go applications. It wraps Bun ORM with connection pooling, migrations, transactions, generic CRUD helpers, rich error handling, and configurable observability.
Package dbkit provides a consistent database layer for Go applications.
DBKit wraps Bun ORM with additional features:
- Connection pooling with full configuration
- Migration execution with checksum verification
- Transaction support with auto commit/rollback and savepoints
- Rich error handling with PostgreSQL error parsing
- Configurable observability (logging, metrics, tracing)
- Health check utilities
Basic Usage ¶
cfg := dbkit.DefaultConfig(os.Getenv("DATABASE_URL"))
cfg.Logger = slog.Default()
cfg.LogSlowQueries = 100 * time.Millisecond
db, err := dbkit.New(cfg)
if err != nil {
log.Fatal(err)
}
defer db.Close()
Migrations ¶
migrations := []dbkit.Migration{
{ID: "001", Description: "Create users", SQL: "CREATE TABLE users (...)"},
{ID: "002", Description: "Add index", SQL: "CREATE INDEX ..."},
}
result, err := db.Migrate(ctx, migrations)
Database Operations ¶
Use Bun ORM directly for all CRUD operations:
// Find by ID
var user User
err := db.NewSelect().Model(&user).Where("id = ?", id).Scan(ctx)
// Find with query
var users []User
err := db.NewSelect().Model(&users).Where("active = ?", true).Order("created_at DESC").Scan(ctx)
// Create
_, err := db.NewInsert().Model(&user).Exec(ctx)
// Update
_, err := db.NewUpdate().Model(&user).WherePK().Exec(ctx)
// Delete
_, err := db.NewDelete().Model(&user).WherePK().Exec(ctx)
Transactions ¶
Callback-based (auto commit/rollback):
err := db.Transaction(ctx, func(tx *dbkit.Tx) error {
if _, err := tx.NewInsert().Model(&user).Exec(ctx); err != nil {
return err // rollback
}
return nil // commit
})
Manual control:
tx, err := db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback()
// ... operations ...
return tx.Commit()
Nested transactions (savepoints):
err := db.Transaction(ctx, func(tx *dbkit.Tx) error {
tx.NewInsert().Model(&outer).Exec(ctx)
err := tx.Transaction(ctx, func(tx2 *dbkit.Tx) error {
return errors.New("fail") // only rolls back inner
})
return nil // outer commits
})
Error Handling ¶
DBKit provides rich error types for PostgreSQL errors:
if _, err := db.NewInsert().Model(&user).Exec(ctx); err != nil {
if dbkit.IsDuplicate(err) {
// Handle duplicate key
}
var dbErr *dbkit.Error
if errors.As(err, &dbErr) {
fmt.Println(dbErr.Code) // DUPLICATE
fmt.Println(dbErr.Constraint) // users_email_key
fmt.Println(dbErr.Detail) // Key (email)=(test@example.com) already exists
}
}
Index ¶
- Constants
- Variables
- func AuditCreate(ctx context.Context, handler AuditHandler, tableName, recordID string, ...) error
- func AuditDelete(ctx context.Context, handler AuditHandler, tableName, recordID string, ...) error
- func AuditUpdate(ctx context.Context, handler AuditHandler, tableName, recordID string, ...) error
- func BatchDelete[T any](ctx context.Context, db bun.IDB, ids []string, batchSize int) (int64, error)
- func BatchInsert[T any](ctx context.Context, db bun.IDB, items []T, batchSize int) (int64, error)
- func BatchUpdate[T any](ctx context.Context, db bun.IDB, items []T, batchSize int) (int64, error)
- func BatchUpsert[T any](ctx context.Context, db bun.IDB, items []T, ...) (int64, error)
- func BulkInsertReturning[T any](ctx context.Context, db bun.IDB, items []T) ([]T, error)
- func CheckVersion[T any](ctx context.Context, db bun.IDB, id string, expectedVersion int64) error
- func Count[T any](ctx context.Context, db bun.IDB, ...) (int, error)
- func CursorPaginate(idColumn, sortColumn, cursor string, limit int, forward bool) func(*bun.SelectQuery) *bun.SelectQuery
- func DefaultUserIDExtractor(ctx context.Context) string
- func DeleteReturning[T any](ctx context.Context, db bun.IDB, model *T) (*T, error)
- func EncodeCursor(id string, sortValue string) string
- func Exists[T any](ctx context.Context, db bun.IDB, ...) (bool, error)
- func FindOrCreate[T any](ctx context.Context, db bun.IDB, model *T, ...) (*T, bool, error)
- func GetColumn(err error) (string, bool)
- func GetConstraint(err error) (string, bool)
- func GetDetail(err error) (string, bool)
- func GetHint(err error) (string, bool)
- func GetTable(err error) (string, bool)
- func GetTenant(ctx context.Context) string
- func HardDelete[T any](ctx context.Context, db bun.IDB, model *T) (sql.Result, error)
- func HardDeleteByID[T any](ctx context.Context, db bun.IDB, id string) (sql.Result, error)
- func InTransaction(ctx context.Context, db *bun.DB, fn func(ctx context.Context, tx bun.Tx) error) error
- func IsCheckViolation(err error) bool
- func IsConflict(err error) bool
- func IsConnection(err error) bool
- func IsDuplicate(err error) bool
- func IsForeignKey(err error) bool
- func IsNotFound(err error) bool
- func IsNotNullViolation(err error) bool
- func IsRetryable(err error) bool
- func IsTimeout(err error) bool
- func KeysetPaginate(column string, lastValue interface{}, limit int) func(*bun.SelectQuery) *bun.SelectQuery
- func NotDeleted(q *bun.SelectQuery) *bun.SelectQuery
- func OnlyDeleted(q *bun.SelectQuery) *bun.SelectQuery
- func Paginate(page, pageSize int) func(*bun.SelectQuery) *bun.SelectQuery
- func Pluck[T any, V any](ctx context.Context, db bun.IDB, column string, ...) ([]V, error)
- func RawExec(ctx context.Context, db bun.IDB, query string, args ...interface{}) (sql.Result, error)
- func RawQuery(ctx context.Context, db bun.IDB, dest interface{}, query string, ...) error
- func RequireTenant(ctx context.Context) (string, error)
- func Restore[T any](ctx context.Context, db bun.IDB, model *T) (sql.Result, error)
- func RestoreByID[T any](ctx context.Context, db bun.IDB, id string) (sql.Result, error)
- func RetryOnConflict(ctx context.Context, maxRetries int, fn func() error) error
- func SetTenantID(ctx context.Context, model interface{}) error
- func SoftDelete[T any](ctx context.Context, db bun.IDB, model *T) (sql.Result, error)
- func SoftDeleteByID[T any](ctx context.Context, db bun.IDB, id string) (sql.Result, error)
- func TenantDeleteScope(ctx context.Context) func(*bun.DeleteQuery) *bun.DeleteQuery
- func TenantScope(ctx context.Context) func(*bun.SelectQuery) *bun.SelectQuery
- func TenantUpdateScope(ctx context.Context) func(*bun.UpdateQuery) *bun.UpdateQuery
- func UpdateColumnsWithVersion[T any](ctx context.Context, db bun.IDB, model *T, version int64, columns ...string) error
- func UpdateReturning[T any](ctx context.Context, db bun.IDB, model *T) (*T, error)
- func UpdateWithVersion[T any](ctx context.Context, db bun.IDB, model *T, version int64) error
- func ValidateTenant(ctx context.Context, db bun.IDB) error
- func WithAuditContext(ctx context.Context, userID, ipAddress, userAgent string) context.Context
- func WithDeleted(q *bun.SelectQuery) *bun.SelectQuery
- func WithTenant(ctx context.Context, tenantID string) context.Context
- type AppliedMigration
- type AuditAction
- type AuditConfig
- type AuditEntry
- type AuditHandler
- type AuditHook
- type AuditLog
- type Auditable
- type AuditableModel
- type BaseModel
- type Config
- type ContextKey
- type Cursor
- type CursorPage
- type DBKit
- func (db *DBKit) Begin(ctx context.Context) (*Tx, error)
- func (db *DBKit) BeginWithOptions(ctx context.Context, opts TxOptions) (*Tx, error)
- func (db *DBKit) Bun() *bun.DB
- func (db *DBKit) Close() error
- func (db *DBKit) Config() Config
- func (db *DBKit) GetAppliedMigrations(ctx context.Context) ([]AppliedMigration, error)
- func (db *DBKit) Health(ctx context.Context) HealthStatus
- func (db *DBKit) IsHealthy(ctx context.Context) bool
- func (db *DBKit) Migrate(ctx context.Context, migrations []Migration) (*MigrationResult, error)
- func (db *DBKit) MigrationStatus(ctx context.Context, migrations []Migration) ([]MigrationStatusEntry, error)
- func (db *DBKit) Ping(ctx context.Context) error
- func (db *DBKit) ReadOnlyTransaction(ctx context.Context, fn TxFunc) error
- func (db *DBKit) Stats() sql.DBStats
- func (db *DBKit) Transaction(ctx context.Context, fn TxFunc) error
- func (db *DBKit) TransactionWithOptions(ctx context.Context, opts TxOptions, fn TxFunc) error
- type Error
- type ErrorCode
- type FullModel
- type HealthStatus
- type IDB
- type Migration
- type MigrationResult
- type MigrationStatusEntry
- type OffsetPage
- type PageInfo
- type PaginationOptions
- type PoolStats
- type QueryResult
- type SoftDeletableModel
- type Tenant
- type TenantConfig
- type TenantContextKey
- type TenantHook
- type TenantIsolation
- type TenantModel
- type TimestampedModel
- type Tx
- func (tx *Tx) Commit() error
- func (tx *Tx) DBKit() *DBKit
- func (tx *Tx) ReleaseSavepoint(ctx context.Context, name string) error
- func (tx *Tx) Rollback() error
- func (tx *Tx) RollbackTo(ctx context.Context, name string) error
- func (tx *Tx) Savepoint(ctx context.Context, name string) error
- func (tx *Tx) Transaction(ctx context.Context, fn TxFunc) error
- type TxFunc
- type TxOptions
- type VersionedModel
- type VersionedUpdate
Constants ¶
const BatchSize = 100
BatchSize is the default batch size for batch operations.
const DefaultPageSize = 20
DefaultPageSize is the default number of items per page.
const MaxPageSize = 100
MaxPageSize is the maximum allowed page size.
Variables ¶
var ( ErrNotFound = errors.New("dbkit: record not found") ErrDuplicate = errors.New("dbkit: duplicate key violation") ErrForeignKey = errors.New("dbkit: foreign key violation") ErrCheckViolation = errors.New("dbkit: check constraint violation") ErrNotNullViolation = errors.New("dbkit: not null violation") ErrConnection = errors.New("dbkit: connection failed") ErrTimeout = errors.New("dbkit: operation timeout") ErrSerialization = errors.New("dbkit: serialization failure") ErrDeadlock = errors.New("dbkit: deadlock detected") )
Sentinel errors for quick checks
var ErrConflict = errors.New("dbkit: optimistic locking conflict - record was modified")
ErrConflict is returned when an optimistic locking conflict is detected.
var ErrNoTenant = errors.New("dbkit: tenant ID not found in context")
ErrNoTenant is returned when tenant ID is required but not found in context.
Functions ¶
func AuditCreate ¶
func AuditCreate(ctx context.Context, handler AuditHandler, tableName, recordID string, newData interface{}) error
AuditCreate logs a create action for a model. Call this after inserting a record.
Usage:
_, err := db.NewInsert().Model(&user).Exec(ctx)
if err == nil {
dbkit.AuditCreate(ctx, auditor, "users", user.ID, &user)
}
func AuditDelete ¶
func AuditDelete(ctx context.Context, handler AuditHandler, tableName, recordID string, oldData interface{}) error
AuditDelete logs a delete action for a model. Call this after deleting a record.
Usage:
dbkit.AuditDelete(ctx, auditor, "users", user.ID, &user) _, err := db.NewDelete().Model(&user).WherePK().Exec(ctx)
func AuditUpdate ¶
func AuditUpdate(ctx context.Context, handler AuditHandler, tableName, recordID string, oldData, newData interface{}) error
AuditUpdate logs an update action for a model. Call this after updating a record.
Usage:
oldUser := user // Copy before update
_, err := db.NewUpdate().Model(&user).WherePK().Exec(ctx)
if err == nil {
dbkit.AuditUpdate(ctx, auditor, "users", user.ID, &oldUser, &user)
}
func BatchDelete ¶
func BatchDelete[T any](ctx context.Context, db bun.IDB, ids []string, batchSize int) (int64, error)
BatchDelete deletes records in batches by their IDs. Returns the total number of rows affected.
Usage:
ids := []string{"id1", "id2", "id3"}
count, err := dbkit.BatchDelete[User](ctx, db, ids, 100)
func BatchInsert ¶
BatchInsert inserts records in batches to avoid exceeding PostgreSQL limits. Returns the total number of rows affected.
Usage:
users := []User{{Name: "A"}, {Name: "B"}, ...}
count, err := dbkit.BatchInsert(ctx, db, users, 100)
func BatchUpdate ¶
BatchUpdate updates records in batches. Returns the total number of rows affected.
Usage:
users := []User{{ID: "1", Name: "Updated1"}, {ID: "2", Name: "Updated2"}}
count, err := dbkit.BatchUpdate(ctx, db, users, 100)
func BatchUpsert ¶
func BatchUpsert[T any](ctx context.Context, db bun.IDB, items []T, conflictColumns, updateColumns []string, batchSize int) (int64, error)
BatchUpsert performs upsert (insert or update) in batches. conflictColumns specifies which columns to check for conflicts. updateColumns specifies which columns to update on conflict.
Usage:
users := []User{{Email: "a@example.com", Name: "A"}, ...}
count, err := dbkit.BatchUpsert(ctx, db, users, []string{"email"}, []string{"name", "updated_at"}, 100)
func BulkInsertReturning ¶
BulkInsertReturning inserts records and returns the inserted rows with generated values.
Usage:
users := []User{{Name: "A"}, {Name: "B"}}
inserted, err := dbkit.BulkInsertReturning(ctx, db, users)
// inserted now has IDs filled in
func CheckVersion ¶
CheckVersion verifies that a record's version matches the expected version. Returns ErrConflict if versions don't match.
Usage:
if err := dbkit.CheckVersion[Account](ctx, db, accountID, expectedVersion); err != nil {
// Version mismatch - reload required
}
func Count ¶
func Count[T any](ctx context.Context, db bun.IDB, queryFn func(*bun.SelectQuery) *bun.SelectQuery) (int, error)
Count returns the count of records matching the query.
Usage:
count, err := dbkit.Count[User](ctx, db, func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("active = ?", true)
})
func CursorPaginate ¶
func CursorPaginate(idColumn, sortColumn, cursor string, limit int, forward bool) func(*bun.SelectQuery) *bun.SelectQuery
CursorPaginate applies cursor-based pagination to a query. The idColumn should be the primary key or a unique column. The sortColumn is optional and used for sorting before the ID.
Usage:
var users []User
db.NewSelect().Model(&users).
Apply(dbkit.CursorPaginate("id", "", afterCursor, 10, true)).
Scan(ctx)
func DefaultUserIDExtractor ¶
DefaultUserIDExtractor extracts the user ID from the context.
func DeleteReturning ¶
DeleteReturning deletes a record and returns the deleted row.
Usage:
deleted, err := dbkit.DeleteReturning(ctx, db, &user)
func EncodeCursor ¶
EncodeCursor encodes a cursor to a base64 string.
func Exists ¶
func Exists[T any](ctx context.Context, db bun.IDB, queryFn func(*bun.SelectQuery) *bun.SelectQuery) (bool, error)
Exists checks if any record matches the query.
Usage:
exists, err := dbkit.Exists[User](ctx, db, func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("email = ?", email)
})
func FindOrCreate ¶
func FindOrCreate[T any](ctx context.Context, db bun.IDB, model *T, findFn func(*bun.SelectQuery) *bun.SelectQuery) (*T, bool, error)
FindOrCreate finds a record or creates it if it doesn't exist. Returns the record and a boolean indicating if it was created.
Usage:
user, created, err := dbkit.FindOrCreate(ctx, db, &User{Email: "test@example.com"},
func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("email = ?", "test@example.com")
})
func GetConstraint ¶
GetConstraint extracts the constraint name if available
func GetTenant ¶
GetTenant extracts tenant ID from the context. Returns empty string if not found.
Usage:
tenantID := dbkit.GetTenant(ctx)
func HardDelete ¶
HardDelete permanently removes a soft-deleted record. This bypasses the soft delete and actually deletes the record.
Usage:
err := dbkit.HardDelete(ctx, db, &user)
func HardDeleteByID ¶
HardDeleteByID permanently removes a record by its ID.
Usage:
err := dbkit.HardDeleteByID[User](ctx, db, userID)
func InTransaction ¶
func InTransaction(ctx context.Context, db *bun.DB, fn func(ctx context.Context, tx bun.Tx) error) error
InTransaction executes a function within a transaction. This is an alias for DBKit.Transaction for use with plain bun.IDB.
Usage:
err := dbkit.InTransaction(ctx, db, func(ctx context.Context, tx bun.Tx) error {
// do work
return nil
})
func IsCheckViolation ¶
IsCheckViolation checks if error is a check constraint error
func IsConflict ¶
IsConflict checks if the error is an optimistic locking conflict.
func IsConnection ¶
IsConnection checks if error is a connection error
func IsDuplicate ¶
IsDuplicate checks if error is a duplicate key error
func IsForeignKey ¶
IsForeignKey checks if error is a foreign key error
func IsNotFound ¶
IsNotFound checks if error is a not found error This also checks for sql.ErrNoRows for direct Bun calls
func IsNotNullViolation ¶
IsNotNullViolation checks if error is a not null violation error
func IsRetryable ¶
IsRetryable checks if the error is retryable (serialization, deadlock)
func KeysetPaginate ¶
func KeysetPaginate(column string, lastValue interface{}, limit int) func(*bun.SelectQuery) *bun.SelectQuery
KeysetPaginate applies keyset pagination (also known as seek method). This is more efficient than offset for large datasets.
Usage:
var users []User
db.NewSelect().Model(&users).
Apply(dbkit.KeysetPaginate("id", lastID, 10)).
Order("id ASC").
Scan(ctx)
func NotDeleted ¶
func NotDeleted(q *bun.SelectQuery) *bun.SelectQuery
NotDeleted returns a query modifier that filters out soft-deleted records. Use this with Bun's query builder to exclude deleted records.
Usage:
var users []User db.NewSelect().Model(&users).Apply(dbkit.NotDeleted).Scan(ctx)
func OnlyDeleted ¶
func OnlyDeleted(q *bun.SelectQuery) *bun.SelectQuery
OnlyDeleted returns a query modifier that includes only soft-deleted records. Use this to find records that have been soft deleted.
Usage:
var deletedUsers []User db.NewSelect().Model(&deletedUsers).Apply(dbkit.OnlyDeleted).Scan(ctx)
func Paginate ¶
func Paginate(page, pageSize int) func(*bun.SelectQuery) *bun.SelectQuery
Paginate applies offset-based pagination to a query. Returns a query modifier that can be used with Apply().
Usage:
var users []User db.NewSelect().Model(&users).Apply(dbkit.Paginate(2, 10)).Scan(ctx)
func Pluck ¶
func Pluck[T any, V any](ctx context.Context, db bun.IDB, column string, queryFn func(*bun.SelectQuery) *bun.SelectQuery) ([]V, error)
Pluck extracts a single column from matching records.
Usage:
emails, err := dbkit.Pluck[User, string](ctx, db, "email", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("active = ?", true)
})
func RawExec ¶
func RawExec(ctx context.Context, db bun.IDB, query string, args ...interface{}) (sql.Result, error)
RawExec executes a raw SQL statement.
Usage:
result, err := dbkit.RawExec(ctx, db, "UPDATE users SET active = ? WHERE last_login < ?", false, cutoffDate)
func RawQuery ¶
func RawQuery(ctx context.Context, db bun.IDB, dest interface{}, query string, args ...interface{}) error
RawQuery executes a raw SQL query and scans results into the destination.
Usage:
var results []map[string]interface{}
err := dbkit.RawQuery(ctx, db, &results, "SELECT * FROM users WHERE age > ?", 18)
func RequireTenant ¶
RequireTenant extracts tenant ID from context or returns an error.
Usage:
tenantID, err := dbkit.RequireTenant(ctx)
func Restore ¶
Restore removes the soft delete mark from a model.
Usage:
err := dbkit.Restore(ctx, db, &user)
func RestoreByID ¶
RestoreByID removes the soft delete mark from a record by its ID.
Usage:
err := dbkit.RestoreByID[User](ctx, db, userID)
func RetryOnConflict ¶
RetryOnConflict executes a function and retries on optimistic locking conflicts. The function should reload the model and retry the operation.
Usage:
err := dbkit.RetryOnConflict(ctx, 3, func() error {
// Reload the record
db.NewSelect().Model(&account).WherePK().Scan(ctx)
// Modify and update
account.Balance += 100
return dbkit.UpdateWithVersion(ctx, db, &account, account.Version)
})
func SetTenantID ¶
SetTenantID sets the tenant ID on a model from context. The model must have a TenantID field.
Usage:
user := &User{Email: "test@example.com"}
dbkit.SetTenantID(ctx, user)
db.NewInsert().Model(user).Exec(ctx)
func SoftDelete ¶
SoftDelete marks a model as deleted by setting the DeletedAt field. The model must embed SoftDeletableModel or have a DeletedAt field.
Usage:
err := dbkit.SoftDelete(ctx, db, &user)
func SoftDeleteByID ¶
SoftDeleteByID marks a record as deleted by its ID.
Usage:
err := dbkit.SoftDeleteByID[User](ctx, db, userID)
func TenantDeleteScope ¶
func TenantDeleteScope(ctx context.Context) func(*bun.DeleteQuery) *bun.DeleteQuery
TenantDeleteScope returns a query modifier for delete queries.
Usage:
db.NewDelete().Model(&user).Apply(dbkit.TenantDeleteScope(ctx)).WherePK().Exec(ctx)
func TenantScope ¶
func TenantScope(ctx context.Context) func(*bun.SelectQuery) *bun.SelectQuery
TenantScope returns a query modifier that filters by tenant ID from context. Use this with Bun's query builder to scope queries to the current tenant.
Usage:
var users []User db.NewSelect().Model(&users).Apply(dbkit.TenantScope(ctx)).Scan(ctx)
func TenantUpdateScope ¶
func TenantUpdateScope(ctx context.Context) func(*bun.UpdateQuery) *bun.UpdateQuery
TenantUpdateScope returns a query modifier for update queries.
Usage:
db.NewUpdate().Model(&user).Apply(dbkit.TenantUpdateScope(ctx)).WherePK().Exec(ctx)
func UpdateColumnsWithVersion ¶
func UpdateColumnsWithVersion[T any](ctx context.Context, db bun.IDB, model *T, version int64, columns ...string) error
UpdateColumnsWithVersion performs an optimistic locking update on specific columns. It increments the version and only succeeds if the current version matches.
Usage:
err := dbkit.UpdateColumnsWithVersion(ctx, db, &account, account.Version, "balance", "updated_at")
func UpdateReturning ¶
UpdateReturning updates a record and returns the updated row.
Usage:
user.Name = "Updated" updated, err := dbkit.UpdateReturning(ctx, db, &user)
func UpdateWithVersion ¶
UpdateWithVersion performs an optimistic locking update. It increments the version and only succeeds if the current version matches. Returns ErrConflict if the record was modified by another process.
Usage:
account.Balance += 100
err := dbkit.UpdateWithVersion(ctx, db, &account)
if errors.Is(err, dbkit.ErrConflict) {
// Handle conflict - reload and retry
}
func ValidateTenant ¶
ValidateTenant checks if the tenant ID in context is valid. Returns error if tenant doesn't exist or is not active.
func WithAuditContext ¶
WithAuditContext adds audit context information to a context.
Usage:
ctx = dbkit.WithAuditContext(ctx, userID, ipAddress, userAgent)
func WithDeleted ¶
func WithDeleted(q *bun.SelectQuery) *bun.SelectQuery
WithDeleted returns a query modifier that includes all records (both deleted and not). This is useful when you need to see all records regardless of deletion status. Note: By default, models with soft_delete tag are automatically filtered.
Usage:
var allUsers []User db.NewSelect().Model(&allUsers).Apply(dbkit.WithDeleted).Scan(ctx)
Types ¶
type AppliedMigration ¶
type AppliedMigration struct {
ID string
Description string
AppliedAt time.Time
Duration time.Duration
Checksum string
}
AppliedMigration represents a successfully applied migration
type AuditAction ¶
type AuditAction string
AuditAction represents the type of action being audited.
const ( AuditActionCreate AuditAction = "CREATE" AuditActionUpdate AuditAction = "UPDATE" AuditActionDelete AuditAction = "DELETE" )
type AuditConfig ¶
type AuditConfig struct {
// Handler is called for each audit entry.
Handler AuditHandler
// Tables specifies which tables to audit. If empty, all tables are audited.
Tables []string
// ExcludeTables specifies tables to exclude from auditing.
ExcludeTables []string
// IncludeOldData includes the old data in update/delete operations.
IncludeOldData bool
// IncludeNewData includes the new data in create/update operations.
IncludeNewData bool
// UserIDExtractor extracts the user ID from the context.
UserIDExtractor func(ctx context.Context) string
// MetadataExtractor extracts additional metadata from the context.
MetadataExtractor func(ctx context.Context) map[string]interface{}
}
AuditConfig configures the audit system.
type AuditEntry ¶
type AuditEntry struct {
ID string `json:"id,omitempty"`
Action AuditAction `json:"action"`
TableName string `json:"table_name"`
RecordID string `json:"record_id"`
OldData json.RawMessage `json:"old_data,omitempty"`
NewData json.RawMessage `json:"new_data,omitempty"`
UserID string `json:"user_id,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
AuditEntry represents a single audit log entry.
type AuditHandler ¶
type AuditHandler func(ctx context.Context, entry *AuditEntry) error
AuditHandler is a function that handles audit entries. Implement this to store audit logs in your preferred backend.
func NewDatabaseAuditHandler ¶
func NewDatabaseAuditHandler(db bun.IDB) AuditHandler
NewDatabaseAuditHandler creates an AuditHandler that stores entries in the database.
Usage:
handler := dbkit.NewDatabaseAuditHandler(db) dbkit.AuditCreate(ctx, handler, "users", user.ID, &user)
type AuditHook ¶
type AuditHook struct {
// contains filtered or unexported fields
}
AuditHook is a Bun query hook that creates audit log entries.
func NewAuditHook ¶
func NewAuditHook(config AuditConfig) *AuditHook
NewAuditHook creates a new audit hook with the given configuration.
func (*AuditHook) CreateEntry ¶
func (h *AuditHook) CreateEntry(ctx context.Context, action AuditAction, tableName, recordID string, oldData, newData interface{}) *AuditEntry
CreateEntry creates an audit entry from the context and query.
type AuditLog ¶
type AuditLog struct {
bun.BaseModel `bun:"table:audit_logs,alias:al"`
ID string `bun:"id,pk,type:uuid,default:gen_random_uuid()"`
Action AuditAction `bun:"action,notnull"`
TableName string `bun:"table_name,notnull"`
RecordID string `bun:"record_id,notnull"`
OldData json.RawMessage `bun:"old_data,type:jsonb"`
NewData json.RawMessage `bun:"new_data,type:jsonb"`
UserID string `bun:"user_id"`
IPAddress string `bun:"ip_address"`
UserAgent string `bun:"user_agent"`
Metadata json.RawMessage `bun:"metadata,type:jsonb"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
}
AuditLog is a database model for storing audit entries. Use this if you want to store audit logs in the database.
Create the table with:
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
action VARCHAR(20) NOT NULL,
table_name VARCHAR(255) NOT NULL,
record_id VARCHAR(255) NOT NULL,
old_data JSONB,
new_data JSONB,
user_id VARCHAR(255),
ip_address VARCHAR(45),
user_agent TEXT,
metadata JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_audit_logs_table_record ON audit_logs(table_name, record_id);
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
type Auditable ¶
type Auditable interface {
// AuditID returns the ID of the record for audit purposes.
AuditID() string
// AuditTableName returns the table name for audit purposes.
AuditTableName() string
}
Auditable is an interface that models can implement to provide audit information.
type AuditableModel ¶
type AuditableModel struct{}
AuditableModel is a base model that implements the Auditable interface. Embed this in your models to enable audit logging.
Usage:
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
dbkit.BaseModel
dbkit.AuditableModel
Email string `bun:"email,notnull,unique"`
}
type BaseModel ¶
type BaseModel struct {
ID string `bun:"id,pk,type:uuid,default:gen_random_uuid()"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`
}
BaseModel provides common fields for all models: ID and timestamps. Embed this in your model structs for standard ID and timestamp handling.
Usage:
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
dbkit.BaseModel
Email string `bun:"email,notnull,unique"`
}
type Config ¶
type Config struct {
// Connection
URL string // PostgreSQL connection string (required)
// Pool settings
MaxOpenConns int // Max open connections (default: 25)
MaxIdleConns int // Max idle connections (default: 5)
ConnMaxLifetime time.Duration // Max connection lifetime (default: 5m)
ConnMaxIdleTime time.Duration // Max idle time (default: 1m)
// Timeouts
DialTimeout time.Duration // Connection dial timeout (default: 5s)
ReadTimeout time.Duration // Read timeout (default: 30s)
WriteTimeout time.Duration // Write timeout (default: 30s)
// Observability (all optional)
Logger *slog.Logger // Structured logger
LogQueries bool // Log all queries
LogSlowQueries time.Duration // Log queries slower than this (0 = disabled)
MetricsRegistry prometheus.Registerer // Prometheus registry for metrics
Tracer trace.Tracer // OpenTelemetry tracer
}
Config holds database configuration
func (Config) WithLogger ¶
WithLogger enables query logging
func (Config) WithMetrics ¶
func (c Config) WithMetrics(registry prometheus.Registerer) Config
WithMetrics enables Prometheus metrics
func (Config) WithSlowQueryLog ¶
WithSlowQueryLog logs queries slower than the threshold
type ContextKey ¶
type ContextKey string
ContextKey is a type for context keys used by the audit system.
const ( // ContextKeyUserID is the context key for the user ID. ContextKeyUserID ContextKey = "dbkit_user_id" // ContextKeyIPAddress is the context key for the IP address. ContextKeyIPAddress ContextKey = "dbkit_ip_address" // ContextKeyUserAgent is the context key for the user agent. ContextKeyUserAgent ContextKey = "dbkit_user_agent" )
type Cursor ¶
Cursor represents a pagination cursor.
func DecodeCursor ¶
DecodeCursor decodes a base64 cursor string.
type CursorPage ¶
CursorPage represents a cursor-based paginated result.
type DBKit ¶
DBKit wraps bun.DB with additional functionality
func (*DBKit) BeginWithOptions ¶
BeginWithOptions starts a new transaction with custom options
func (*DBKit) GetAppliedMigrations ¶
func (db *DBKit) GetAppliedMigrations(ctx context.Context) ([]AppliedMigration, error)
GetAppliedMigrations returns all migrations that have been applied
func (*DBKit) Health ¶
func (db *DBKit) Health(ctx context.Context) HealthStatus
Health performs a health check with detailed status
func (*DBKit) MigrationStatus ¶
func (db *DBKit) MigrationStatus(ctx context.Context, migrations []Migration) ([]MigrationStatusEntry, error)
MigrationStatus returns the status of all known migrations
func (*DBKit) ReadOnlyTransaction ¶
ReadOnlyTransaction executes fn within a read-only transaction
func (*DBKit) Transaction ¶
Transaction executes fn within a transaction with automatic commit/rollback
type Error ¶
type Error struct {
Code ErrorCode // Error classification
Message string // Human-readable message
Op string // Operation that failed (e.g., "FindByID", "Create")
Table string // Table name if known
Column string // Column name if known
Constraint string // Constraint name if applicable
Detail string // Additional detail from PostgreSQL
Hint string // Hint from PostgreSQL
Query string // Query that failed (may be empty for security)
Cause error // Underlying error
}
Error is a rich database error with context
type ErrorCode ¶
type ErrorCode string
ErrorCode represents a database error classification
const ( CodeNotFound ErrorCode = "NOT_FOUND" CodeDuplicate ErrorCode = "DUPLICATE" CodeForeignKey ErrorCode = "FOREIGN_KEY" CodeCheckViolation ErrorCode = "CHECK_VIOLATION" CodeNotNullViolation ErrorCode = "NOT_NULL" CodeConnectionFailed ErrorCode = "CONNECTION_FAILED" CodeTimeout ErrorCode = "TIMEOUT" CodeSerialization ErrorCode = "SERIALIZATION" CodeDeadlock ErrorCode = "DEADLOCK" CodeConflict ErrorCode = "CONFLICT" CodeUnknown ErrorCode = "UNKNOWN" )
func GetErrorCode ¶
GetErrorCode extracts the error code if it's a dbkit error
type FullModel ¶
type FullModel struct {
ID string `bun:"id,pk,type:uuid,default:gen_random_uuid()"`
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at,soft_delete,nullzero"`
Version int64 `bun:"version,notnull,default:1"`
}
FullModel combines BaseModel, SoftDeletableModel, and VersionedModel. Use this for models that need all features.
Usage:
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
dbkit.FullModel
Email string `bun:"email,notnull,unique"`
}
func (*FullModel) BeforeAppendModel ¶
type HealthStatus ¶
type HealthStatus struct {
Healthy bool `json:"healthy"`
Latency time.Duration `json:"latency"`
Error string `json:"error,omitempty"`
PoolStats PoolStats `json:"pool_stats"`
}
HealthStatus represents the database health status
type IDB ¶
type IDB interface {
bun.IDB
NewSelect() *bun.SelectQuery
NewInsert() *bun.InsertQuery
NewUpdate() *bun.UpdateQuery
NewDelete() *bun.DeleteQuery
NewRaw(query string, args ...any) *bun.RawQuery
NewCreateTable() *bun.CreateTableQuery
NewDropTable() *bun.DropTableQuery
NewCreateIndex() *bun.CreateIndexQuery
NewDropIndex() *bun.DropIndexQuery
NewTruncateTable() *bun.TruncateTableQuery
NewAddColumn() *bun.AddColumnQuery
NewDropColumn() *bun.DropColumnQuery
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
IDB is the interface for both DB and Tx to enable function reuse
type Migration ¶
type Migration struct {
ID string // Unique identifier (e.g., "001", "20240115120000", or any string)
Description string // Human-readable description
SQL string // SQL statements to execute
}
Migration represents a single migration to execute
type MigrationResult ¶
type MigrationResult struct {
Applied []AppliedMigration
Skipped []string // IDs that were already applied
TotalTime time.Duration
}
MigrationResult represents the result of running migrations
type MigrationStatusEntry ¶
type MigrationStatusEntry struct {
ID string
Description string
Checksum string
Applied bool
ChecksumMatch bool // Only relevant if Applied is true
}
MigrationStatusEntry represents the status of a single migration
type OffsetPage ¶
type OffsetPage[T any] struct { Items []T `json:"items"` Page int `json:"page"` PageSize int `json:"page_size"` TotalItems int `json:"total_items"` TotalPages int `json:"total_pages"` PageInfo PageInfo `json:"page_info"` }
OffsetPage represents an offset-based paginated result.
func PaginateWithCount ¶
func PaginateWithCount[T any](ctx context.Context, db bun.IDB, page, pageSize int, queryFn func(*bun.SelectQuery) *bun.SelectQuery) (*OffsetPage[T], error)
PaginateWithCount executes an offset-paginated query and returns results with metadata.
Usage:
page, err := dbkit.PaginateWithCount[User](ctx, db, 1, 10, func(q *bun.SelectQuery) *bun.SelectQuery {
return q.Where("active = ?", true).Order("created_at DESC")
})
type PageInfo ¶
type PageInfo struct {
HasNextPage bool `json:"has_next_page"`
HasPreviousPage bool `json:"has_previous_page"`
StartCursor string `json:"start_cursor,omitempty"`
EndCursor string `json:"end_cursor,omitempty"`
TotalCount int `json:"total_count,omitempty"`
}
PageInfo contains pagination metadata.
func CursorPaginateResult ¶
func CursorPaginateResult[T any](items []T, limit int, forward bool, cursorFn func(T) string) ([]T, PageInfo)
CursorPaginateResult processes cursor pagination results and builds page info. Pass the items fetched with limit+1, and it will trim and determine hasMore.
Usage:
items, pageInfo := dbkit.CursorPaginateResult(users, 10, true, func(u User) string {
return dbkit.EncodeCursor(u.ID, "")
})
type PaginationOptions ¶
type PaginationOptions struct {
// Page number (1-indexed) for offset pagination
Page int
// PageSize is the number of items per page
PageSize int
// After cursor for forward cursor pagination
After string
// Before cursor for backward cursor pagination
Before string
// First N items (cursor pagination)
First int
// Last N items (cursor pagination)
Last int
// IncludeTotalCount includes total count in response (can be expensive)
IncludeTotalCount bool
}
PaginationOptions configures pagination behavior.
type PoolStats ¶
type PoolStats struct {
MaxOpenConnections int `json:"max_open_connections"`
OpenConnections int `json:"open_connections"`
InUse int `json:"in_use"`
Idle int `json:"idle"`
WaitCount int64 `json:"wait_count"`
WaitDuration time.Duration `json:"wait_duration"`
MaxIdleClosed int64 `json:"max_idle_closed"`
MaxIdleTimeClosed int64 `json:"max_idle_time_closed"`
MaxLifetimeClosed int64 `json:"max_lifetime_closed"`
}
PoolStats contains connection pool statistics
func PoolStatsFromSQL ¶
PoolStatsFromSQL converts sql.DBStats to PoolStats
type QueryResult ¶
type QueryResult[T any] struct { // contains filtered or unexported fields }
QueryResult wraps a query result with error context for chainable error handling. It provides a way to add meaningful context to errors without depending on Bun internals.
func WithErr ¶
func WithErr[T any](result T, err error, op string) *QueryResult[T]
WithErr wraps a result and error with operation context for enhanced error handling. This function allows chainable error handling with meaningful context.
Usage:
// For operations that return (sql.Result, error)
result, err := dbkit.WithErr(db.NewInsert().Model(&user).Exec(ctx), "CreateUser").Unwrap()
// For operations that return only error (like Scan)
err := dbkit.WithErr1(db.NewSelect().Model(&user).Where("id = ?", id).Scan(ctx), "FindByID").Err()
// Check error directly
if dbkit.WithErr(db.NewInsert().Model(&user).Exec(ctx), "CreateUser").HasError() {
// handle error
}
func WithErr1 ¶
func WithErr1(err error, op string) *QueryResult[struct{}]
WithErr1 is a convenience function for operations that return only an error. This is useful for Scan() operations which don't return a result.
Usage:
err := dbkit.WithErr1(db.NewSelect().Model(&user).Where("id = ?", id).Scan(ctx), "FindByID").Err()
func (*QueryResult[T]) Err ¶
func (qr *QueryResult[T]) Err() error
Err returns the wrapped error with enhanced context. If there was no error, it returns nil.
func (*QueryResult[T]) HasError ¶
func (qr *QueryResult[T]) HasError() bool
HasError returns true if there was an error.
func (*QueryResult[T]) Result ¶
func (qr *QueryResult[T]) Result() T
Result returns only the result value. Use Err() to check for errors first.
func (*QueryResult[T]) Unwrap ¶
func (qr *QueryResult[T]) Unwrap() (T, error)
Unwrap returns the result and the wrapped error. Use this when you need both the result and the error.
type SoftDeletableModel ¶
SoftDeletableModel adds soft delete capability to models. Embed this alongside BaseModel for soft delete functionality.
Usage:
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
dbkit.BaseModel
dbkit.SoftDeletableModel
Email string `bun:"email,notnull,unique"`
}
When querying, add a filter to exclude soft-deleted records:
db.NewSelect().Model(&users).Where("deleted_at IS NULL").Scan(ctx)
To soft delete:
db.NewUpdate().Model(&user).Set("deleted_at = ?", time.Now()).WherePK().Exec(ctx)
To restore:
db.NewUpdate().Model(&user).Set("deleted_at = NULL").WherePK().Exec(ctx)
func (*SoftDeletableModel) IsDeleted ¶
func (m *SoftDeletableModel) IsDeleted() bool
IsDeleted returns true if the model has been soft deleted.
type Tenant ¶
type Tenant struct {
bun.BaseModel `bun:"table:tenants,alias:t"`
ID string `bun:"id,pk,type:uuid,default:gen_random_uuid()"`
Name string `bun:"name,notnull"`
Subdomain string `bun:"subdomain,notnull,unique"`
Active bool `bun:"active,notnull,default:true"`
Metadata string `bun:"metadata,type:jsonb"`
TimestampedModel
}
Tenant represents a tenant entity.
type TenantConfig ¶
type TenantConfig struct {
// Column is the tenant ID column name
Column string
// EnforceOnSelect automatically filters SELECT queries by tenant
EnforceOnSelect bool
// EnforceOnUpdate automatically filters UPDATE queries by tenant
EnforceOnUpdate bool
// EnforceOnDelete automatically filters DELETE queries by tenant
EnforceOnDelete bool
// SetOnInsert automatically sets tenant ID on INSERT
SetOnInsert bool
}
TenantConfig configures multi-tenancy behavior.
func DefaultTenantConfig ¶
func DefaultTenantConfig() TenantConfig
DefaultTenantConfig returns the default tenant configuration.
type TenantContextKey ¶
type TenantContextKey struct{}
TenantContextKey is the context key for tenant ID.
type TenantHook ¶
type TenantHook struct {
// Column is the tenant ID column name (default: "tenant_id")
Column string
}
TenantHook is a Bun query hook that automatically applies tenant filtering.
func NewTenantHook ¶
func NewTenantHook(column string) *TenantHook
NewTenantHook creates a new tenant hook.
type TenantIsolation ¶
type TenantIsolation struct {
// contains filtered or unexported fields
}
TenantIsolation provides methods for tenant-isolated operations.
func NewTenantIsolation ¶
func NewTenantIsolation(db bun.IDB, config TenantConfig) *TenantIsolation
NewTenantIsolation creates a new tenant isolation helper.
func (*TenantIsolation) Delete ¶
func (ti *TenantIsolation) Delete(ctx context.Context) *bun.DeleteQuery
Delete creates a tenant-scoped DELETE query.
Usage:
ti.Delete(ctx).Model(&user).WherePK().Exec(ctx)
func (*TenantIsolation) Insert ¶
func (ti *TenantIsolation) Insert(ctx context.Context) *bun.InsertQuery
Insert creates a query and can set tenant ID automatically. Note: You should still set the tenant ID on the model before insert.
func (*TenantIsolation) Select ¶
func (ti *TenantIsolation) Select(ctx context.Context) *bun.SelectQuery
Select creates a tenant-scoped SELECT query.
Usage:
var users []User ti.Select(ctx).Model(&users).Scan(ctx)
func (*TenantIsolation) Update ¶
func (ti *TenantIsolation) Update(ctx context.Context) *bun.UpdateQuery
Update creates a tenant-scoped UPDATE query.
Usage:
ti.Update(ctx).Model(&user).WherePK().Exec(ctx)
type TenantModel ¶
type TenantModel struct {
TenantID string `bun:"tenant_id,notnull"`
}
TenantModel provides tenant isolation for models. Embed this in your model structs to add tenant_id field.
Usage:
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
dbkit.BaseModel
dbkit.TenantModel
Email string `bun:"email,notnull"`
}
func (*TenantModel) SetTenantID ¶
func (m *TenantModel) SetTenantID(tenantID string)
SetTenantID sets the tenant ID on the model.
type TimestampedModel ¶
type TimestampedModel struct {
CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"`
}
TimestampedModel is an alias for BaseModel for clarity. Use this if you only need timestamps without the ID field.
Usage:
type AuditLog struct {
bun.BaseModel `bun:"table:audit_logs,alias:al"`
ID int64 `bun:"id,pk,autoincrement"`
dbkit.TimestampedModel
Action string `bun:"action,notnull"`
}
func (*TimestampedModel) BeforeAppendModel ¶
type Tx ¶
Tx wraps bun.Tx with additional functionality
func (*Tx) ReleaseSavepoint ¶
ReleaseSavepoint releases a named savepoint
func (*Tx) RollbackTo ¶
RollbackTo rolls back to a named savepoint
type TxOptions ¶
type TxOptions struct {
Isolation sql.IsolationLevel
ReadOnly bool
}
TxOptions configures transaction behavior
func DefaultTxOptions ¶
func DefaultTxOptions() TxOptions
DefaultTxOptions returns default transaction options
func ReadOnlyTxOptions ¶
func ReadOnlyTxOptions() TxOptions
ReadOnlyTxOptions returns options for read-only transactions
func SerializableTxOptions ¶
func SerializableTxOptions() TxOptions
SerializableTxOptions returns options for serializable transactions
type VersionedModel ¶
type VersionedModel struct {
Version int64 `bun:"version,notnull,default:1"`
}
VersionedModel adds optimistic locking capability to models. Embed this alongside BaseModel for version-based conflict detection.
Usage:
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
dbkit.BaseModel
dbkit.VersionedModel
Email string `bun:"email,notnull,unique"`
}
When updating, include version check:
result, err := db.NewUpdate().
Model(&user).
Set("version = version + 1").
Where("id = ?", user.ID).
Where("version = ?", user.Version).
Exec(ctx)
Check if update was successful:
rows, _ := result.RowsAffected()
if rows == 0 {
// Conflict detected - record was modified by another process
}
type VersionedUpdate ¶
type VersionedUpdate[T any] struct { // contains filtered or unexported fields }
VersionedUpdate is a helper struct for building versioned update queries.
func NewVersionedUpdate ¶
func NewVersionedUpdate[T any](db bun.IDB, model *T, version int64) *VersionedUpdate[T]
NewVersionedUpdate creates a new versioned update builder.
func (*VersionedUpdate[T]) Columns ¶
func (v *VersionedUpdate[T]) Columns(cols ...string) *VersionedUpdate[T]
Columns specifies which columns to update.