dbx

package
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 12 Imported by: 0

README

dbx

dbx 是 Aisphere Kernel 的统一数据库访问模块。它在 GORM 之上封装了八项开箱即用的"预先动作",让业务代码不用再重复 aisphere-hub 里那 100+ 处样板。Kernel 包开箱即用,业务代码拿过去就能跑,而且用得特别好。

新手上路:只看本文件即可上手。需要深度细节时再翻其他文档(见末尾"文档地图")。


1. 为什么需要 dbx

aisphere-hub 的 internal/data/skill.go(1687 行)+ skillset.go(500 行)里,我们数到至少 108 处重复样板:

重复样板 出现次数 dbx 的解决方式
r.db(ctx) helper(优先用 ctx 里的 tx,否则用全局 DB) 每个 repo 一份 DB.GORM(ctx) 内置 context 透传
isUniqueViolation(err) 检查 pgconn.PgError 23505 6+ 处 dbx.ErrDuplicateKey 自动归一化
if res.RowsAffected == 0 { return ErrXxxNotFound } 10+ 处 dbx.AssertAffected(res) 一行搞定
clause.OnConflict{ DoUpdates: ... } + SECURITY 白名单注释 3+ 处 db.SafeUpsert(row, allowedColumns)
Unscoped() 查已删除记录(误用风险) 5+ 处 dbx.WithUnscoped(ctx) 显式 opt-in
time.Now() 填充 updated_at 10+ 处 GORM autoUpdateTime tag 自动处理
UpdateColumn("count", gorm.Expr("count + ?", delta)) 原子计数 2+ 处 db.Increment(model, where, col, delta)
limit+1 + hasMore + NextOffset 分页样板 3+ 处 db.Paginate(ctx, &out, model, where, page, size)
mapSkillDBError 翻译 42P01 undefined table 2+ 处 dbx.ErrSchemaNotReady 提示运行 migration

dbx 把这些样板收敛成一套 DB / Tx 接口,底层用 GORM 的全部能力(链式 Where、Preload、Relations、Hooks、Clauses),业务代码只见 dbx 的类型安全 API。

configx → dbx.Config
  ↓ dbx.New
dbx.DB (interface, 8 项内置能力)
  ↓ FindOne / FindMany / Create / Save / Update / Delete / SafeUpsert / Increment / Paginate / InTx
dbx.Tx (interface, 同上 + Commit/Rollback)
  ↓
dbx/postgres / dbx/mysql driver subpackages (register via init())
  ↓
GORM → database/sql → pgx / go-sql-driver/mysql

dbx 本身不负责 schema migration(提供 AutoMigrate 入口给 dev 环境,生产用 SQL migration)、ORM 关系映射的复杂查询(用 db.GORM(ctx) 逃生口)、缓存(用 cachex 单独管)。


2. 30 秒上手

package main

import (
    "log"
    "time"

    "github.com/aisphereio/kernel/configx"
    "github.com/aisphereio/kernel/configx/file"
    "github.com/aisphereio/kernel/dbx"
    _ "github.com/aisphereio/kernel/dbx/postgres" // 注册 "postgres" driver
)

type DBConfig struct {
    Driver          string `json:"driver"`
    DSN             string `json:"dsn"`
    MaxOpenConns    int    `json:"max_open_conns"`
    MaxIdleConns    int    `json:"max_idle_conns"`
    ConnMaxLifetime int64  `json:"conn_max_lifetime_ns"`
    QueryTimeout    int64  `json:"query_timeout_ns"`
}

func main() {
    cfg := configx.New(configx.WithSource(file.NewSource("configs/app.yaml")))
    defer cfg.Close()
    if err := cfg.Load(); err != nil { log.Fatal(err) }

    var dbCfg DBConfig
    if err := cfg.Value("database").Scan(&dbCfg); err != nil { log.Fatal(err) }

    db, err := dbx.New(dbx.Config{
        Driver:          dbCfg.Driver,
        DSN:             dbCfg.DSN,
        MaxOpenConns:    dbCfg.MaxOpenConns,
        MaxIdleConns:    dbCfg.MaxIdleConns,
        ConnMaxLifetime: time.Duration(dbCfg.ConnMaxLifetime),
        QueryTimeout:    time.Duration(dbCfg.QueryTimeout),
        SlowQueryThreshold: 200 * time.Millisecond,
    })
    if err != nil { log.Fatal(err) }
    defer db.Close()
    // ... pass db to repositories
}

业务代码使用:

type Skill struct {
    ID          int64          `gorm:"primaryKey;autoIncrement;column:id"`
    Name        string         `gorm:"column:name;size:128;uniqueIndex;not null"`
    DisplayName string         `gorm:"column:display_name;size:256;not null;default:''"`
    OwnerID     string         `gorm:"column:owner_id;size:128;not null;default:''"`
    Status      string         `gorm:"column:status;size:32;not null;default:'active'"`
    CreatedAt   time.Time      `gorm:"column:created_at;not null;autoCreateTime"`
    UpdatedAt   time.Time      `gorm:"column:updated_at;not null;autoUpdateTime"`
    DeletedAt   gorm.DeletedAt `gorm:"column:deleted_at;index"`
}

type SkillRepo struct{ db dbx.DB }

func (r *SkillRepo) Find(ctx context.Context, name string) (*Skill, error) {
    var skill Skill
    err := r.db.FindOne(ctx, &skill, "name = ?", name)
    if errors.Is(err, dbx.ErrNoRows) {
        return nil, errorx.NotFound("AIHUB_SKILL_NOT_FOUND", "技能不存在",
            errorx.WithMetadata("skill_id", name),
        )
    }
    if err != nil {
        return nil, errorx.Wrap(err, "AIHUB_SKILL_QUERY_FAILED",
            errorx.WithMessage("查询技能失败"),
            errorx.WithRetryable(true),
        )
    }
    return &skill, nil
}

3. 八项内置能力速查

能力 配置 自动行为
错误归一化 默认开启 gorm.ErrRecordNotFounddbx.ErrNoRows;PG 23505 / MySQL 1062 → dbx.ErrDuplicateKey;PG 42P01 / MySQL 1146 → dbx.ErrSchemaNotReady;ctx 超时 → dbx.ErrTimeout
Soft Delete 安全 默认开启 模型有 DeletedAt 字段时,普通查询自动过滤已删除行;dbx.WithUnscoped(ctx) 才能查已删除
OnConflict 白名单 db.SafeUpsert(row, allowedColumns) owner_id / created_at / deleted_at 永远不被覆盖,即使误列入白名单也会报 ErrUnsafeUpsert
Context 透传 默认开启 DB.GORM(ctx) 自动优先用 InjectDB 注入的请求级 tx;repo 不用每个方法写 db(ctx) helper
审计 Hook Config.AuditEnabled = true BeforeCreate / AfterUpdate / BeforeDelete 自动调 auditx(需 auditx 集成)
QueryTimeout 全局 Config.QueryTimeout > 0 ctx 无 deadline 时自动加 timeout;有 deadline 时不重复加
Slow Query Log Config.SlowQueryThreshold > 0 超阈值查询自动 logx.Warn,带 SQL 文本(截断 500 字符)、耗时、影响行数
Metrics 埋点 Config.MetricsEnabled = true 每个查询自动 metricsx.Histogram(db_query_duration, labels=driver/op/status)(需 metricsx 集成,占位)

4. 构造器速查

场景 API 说明
创建 DB dbx.New(cfg) 打开连接池,注册 driver,装配 8 项能力
关闭 DB db.Close() 幂等
健康检查 db.PingContext(ctx) liveness probe
池状态 db.Stats() sql.DBStats
driver 名 db.DriverName() "postgres" / "mysql"
逃生口 db.GORM(ctx) 返回 *gorm.DB,用于 Preload / 复杂 Where / 原生 SQL
AutoMigrate db.AutoMigrate(ctx, &Model{}) dev 环境快速建表,生产禁用
列表表 db.Tables(ctx) 返回当前 schema 的所有表名

5. CRUD API 速查

单行查询
// 按条件
var skill Skill
err := db.FindOne(ctx, &skill, "name = ? AND status = ?", name, "active")

// 按主键
err := db.FindOneByPK(ctx, &skill, skillID)

// 多列复合主键
err := db.FindOneByPK(ctx, &version, map[string]any{"skill_name": name, "version": v})

FindOne 找不到行返回 dbx.ErrNoRows

多行查询
var skills []Skill
err := db.FindMany(ctx, &skills, "owner_id = ? AND status = ?", ownerID, "active")
写操作
// Create(GORM 自动填 created_at / updated_at)
skill := &Skill{Name: "demo", OwnerID: "user_123"}
err := db.Create(ctx, skill)
// skill.ID 自动填充

// Save(全字段更新或插入)
err := db.Save(ctx, skill)

// Update(部分字段,自动填 updated_at)
err := db.Update(ctx, &Skill{}, "name = ?", []any{name}, map[string]any{
    "display_name": "New Name",
    "status":       "archived",
})

// Delete(软删除 if DeletedAt 字段存在,否则硬删除)
err := db.Delete(ctx, &Skill{}, "name = ?", name)
err := db.DeleteByPK(ctx, &Skill{}, skillID)
SafeUpsert(安全 upsert)
// INSERT ... ON CONFLICT DO UPDATE (PG) / INSERT ... ON DUPLICATE KEY UPDATE (MySQL)
// 只有 display_name / status 会被更新;owner_id 永远不被覆盖
err := db.SafeUpsert(ctx, skill, []string{"display_name", "status"})

// 误把 owner_id 列入白名单会直接报错
err := db.SafeUpsert(ctx, skill, []string{"display_name", "owner_id"})
// → dbx.ErrUnsafeUpsert: column "owner_id" is protected
Increment(原子计数)
// UPDATE ... SET download_count = download_count + 1 WHERE ...
err := db.Increment(ctx, &Skill{}, "name = ?", []any{name}, "download_count", 1)
Paginate(分页)
var page []Skill
res, err := db.Paginate(ctx, &page, &Skill{}, "owner_id = ?", []any{ownerID}, 1, 20)
// res.Total = 156
// res.Page = 1, res.Size = 20
// res.HasMore = true
// len(page) = 20
事务
// 回调式(推荐)
err := db.InTx(ctx, func(tx dbx.Tx) error {
    if err := tx.Create(ctx, skillRow); err != nil { return err }
    if err := tx.Create(ctx, versionRow); err != nil { return err }
    return nil // commit
})
// InTx 自动:fn 返回 nil → commit;返回 error → rollback;panic → rollback + re-panic

// 手动式
tx, err := db.BeginTx(ctx)
defer tx.Rollback() // 幂等,Commit 后变成 no-op
if err := tx.Create(ctx, row); err != nil { return err }
return tx.Commit()

6. driver 注册

import (
    "github.com/aisphereio/kernel/dbx"
    _ "github.com/aisphereio/kernel/dbx/postgres"  // 注册 "postgres"
    // _ "github.com/aisphereio/kernel/dbx/mysql"   // 注册 "mysql"
)
Driver 底层 错误检测
postgres dbx/postgres gorm.io/driver/postgres (pgx/stdlib) pgconn.PgError.Code: 23505 / 23503 / 42P01
mysql dbx/mysql gorm.io/driver/mysql (go-sql-driver) mysql.MySQLError.Number: 1062 / 1452 / 1146

driver 重复注册会 panic(init 期暴露 bug)。


7. 错误归一化速查

dbx 错误 触发条件 推荐 errorx 构造器 retryable
ErrNoRows FindOne 无结果 errorx.NotFound false
ErrDuplicateKey 唯一约束冲突 errorx.Conflict false
ErrTimeout context 超时 errorx.Timeout true
ErrSchemaNotReady 表不存在(未跑 migration) errorx.Internal + 提示 false
ErrForeignKeyViolation 外键约束失败 errorx.BadRequest false
ErrClosed DB 已关闭 errorx.Unavailable true
ErrTxCommitted Tx 已 Commit 后操作 bug,不应出现 false
ErrTxRolledBack Tx 已 Rollback 后操作 bug,不应出现 false
ErrUnsafeUpsert SafeUpsert 白名单含保护列 bug,代码错误 false
ErrNoEffect AssertAffected 检测 0 行 errorx.NotFound false

所有 sentinel 都支持 errors.Is 链式匹配,即使被 fmt.Errorf("%w: ...", err) 包装。


8. Context 透传模式

aisphere-hub 的 db(ctx) 模式内置成 DB.GORM(ctx):

// 启动期:把全局 DB 装进 repo
type SkillRepo struct{ db dbx.DB }

// 请求期:在 handler 边界注入 tx 到 ctx
func (h *Handler) Create(w, r) {
    ctx := r.Context()
    // 如果需要跨多个 repo 的事务,在 service 层开 tx 并注入:
    err := h.db.InTx(ctx, func(tx dbx.Tx) error {
        // tx 自动通过 InjectTx 注入到 ctx;下游 repo 调 r.db.FindOne(ctx, ...)
        // 会自动用这个 tx,不需要显式传 tx 参数
        return h.skillRepo.CreateWithCtx(ctx, skill)
    })
}

// repo 实现:不需要自己写 db(ctx) helper
func (r *SkillRepo) Find(ctx context.Context, name string) (*Skill, error) {
    var skill Skill
    // r.db.FindOne(ctx, ...) 内部调 r.db.GORM(ctx),
    // GORM(ctx) 自动优先用 ctx 里的 tx,否则用全局 DB
    err := r.db.FindOne(ctx, &skill, "name = ?", name)
    // ...
}

9. Soft Delete 安全模式

模型有 gorm.DeletedAt 字段时,dbx 自动启用 soft delete:

type Skill struct {
    // ...
    DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index"`
}

// 普通查询:自动过滤已删除行
db.FindOne(ctx, &skill, "name = ?", name)  // 不会返回已删除的 skill

// 显式查已删除行(必须 opt-in)
unscopedCtx := dbx.WithUnscoped(ctx)
db.FindOne(unscopedCtx, &skill, "name = ?", name)  // 会返回已删除的 skill

// Delete 默认是软删除
db.Delete(ctx, &Skill{}, "name = ?", name)  // UPDATE ... SET deleted_at = NOW()

// 硬删除:用 GORM 逃生口
db.GORM(ctx).Unscoped().Delete(&Skill{}, "name = ?", name)

10. 配置推荐

10.1 连接池大小
工作负载 MaxOpenConns MaxIdleConns
小服务 / 低 QPS 8 4
中等流量 16-32 8-16
高流量 / 短查询 32-64 16-32
高流量 / 长查询 64-128 32-64

经验法则:

  • MaxIdleConns >= MaxOpenConns / 2
  • ConnMaxLifetime 15-30 min(让 PG 故障切换有机会重新路由)
  • ConnMaxIdleTime 2-5 min(避免空闲连接占 PG max_connections)
  • QueryTimeout 5-10 秒
  • SlowQueryThreshold 200ms(生产)/ 50ms(debug)
10.2 Postgres DSN
postgres://user:pass@host:5432/dbname?sslmode=disable&connect_timeout=5
postgresql://user:pass@host:5432/dbname?sslmode=require
host=localhost port=5432 user=user password=pass dbname=app sslmode=disable
10.3 MySQL DSN
user:pass@tcp(host:3306)/dbname?parseTime=true&loc=Local&charset=utf8mb4

parseTime=true 必须,否则 time.Time 字段扫描失败。

10.4 完整 YAML 配置示例
database:
  driver: postgres
  dsn: postgres://user:pass@localhost:5432/app?sslmode=disable
  max_open_conns: 32
  max_idle_conns: 8
  conn_max_lifetime_ns: 1800000000000   # 30 min
  conn_max_idle_time_ns: 300000000000   # 5 min
  query_timeout_ns: 5000000000          # 5 s
  slow_query_threshold_ns: 200000000    # 200 ms
  audit_enabled: false                  # opt-in
  metrics_enabled: false                # opt-in
  dry_run: false
  debug: false

11. AutoMigrate(dev only)

// dev 环境:快速建表
if err := db.AutoMigrate(ctx, &Skill{}, &SkillVersion{}, &SkillFile{}); err != nil {
    log.Fatal(err)
}

// 生产环境:用 SQL migration(golang-migrate / goose)
// dbx 不管 schema;aisphere-hub 风格 migrations/postgres/*.sql

AutoMigrate 是幂等的,重复调用不会破坏数据。但它:

  • 删除列或表
  • 修改列类型(只新增)
  • 创建外键约束(DisableForeignKeyConstraintWhenMigrating: true)

生产环境永远用 SQL migration。AutoMigrate 只用于 dev 快速迭代和测试。


12. 禁止模式

// ❌ 业务代码直接 import database/sql
import "database/sql"

// ❌ 业务代码直接 import GORM
import "gorm.io/gorm"
db, _ := gorm.Open(...)

// ❌ 业务代码直接 import driver
import _ "github.com/jackc/pgx/v5/stdlib"
import _ "github.com/go-sql-driver/mysql"

// ❌ 业务代码用 sqlx
import "github.com/jmoiron/sqlx"

// ❌ 在 handler / service 写 SQL
func (h *Handler) ServeHTTP(w, r) {
    h.db.GORM(r.Context()).Exec("INSERT INTO skills ...")  // SQL 漏到 handler
}

// ❌ 吞掉 ErrNoRows
err := db.FindOne(ctx, &skill, q, id)
if err != nil { return nil, err }  // ErrNoRows 也被当 error 上抛

// ❌ 忘记 defer Close
db, _ := dbx.New(cfg)
// 没有 defer db.Close()

// ❌ Unscoped 没用 WithUnscoped
db.GORM(ctx).Unscoped().Find(&skills)  // 绕过安全门

替代:

// ✅ 用 dbx.New
db, _ := dbx.New(dbx.Config{Driver: "postgres", DSN: dsn})
defer db.Close()

// ✅ SQL 只在 repository
func (r *SkillRepo) Find(ctx context.Context, id string) (*Skill, error) {
    var s Skill
    err := r.db.FindOne(ctx, &s, "id = ?", id)
    // ...
}

// ✅ ErrNoRows 单独处理
if errors.Is(err, dbx.ErrNoRows) { return nil, errorx.NotFound(...) }

// ✅ Unscoped 显式 opt-in
db.FindOne(dbx.WithUnscoped(ctx), &skill, "name = ?", name)

13. 文档地图

快速上手
├── 本文件 (dbx/README.md)                  ← 单一入口
├── dbx/doc.go                              ← go doc 输出源
├── dbx/example_test.go                     ← Go 标准示例
└── dbx/example_business_test.go            ← 业务场景示例

深度规范
├── docs/design/dbx.md                      ← 设计规范
└── docs/contracts/dbx.md                   ← 不可破坏契约

AI 编码指南
├── docs/ai/dbx.md                          ← AI 编码指南
└── AGENTS.md                               ← 项目级 AI 规则

验收与运维
└── docs/process/dbx-acceptance-checklist.md

可运行示例
└── examples/dbx-basic/                     ← aisphere-hub skill 场景示例

14. 发版前检查

# 单元测试(不需要 DB)
go test ./dbx -count=1 -short

# 集成测试(需要 Docker for testcontainers,或设 KERNEL_DBX_PG_DSN / KERNEL_DBX_MYSQL_DSN)
go test ./dbx -count=1

# 性能基准
go test ./dbx -bench=. -benchmem

# go vet
go vet ./dbx/...

# 禁止模式扫描
./scripts/check-dbx-usage.sh

15. 设计哲学一句话

Kernel 开箱即用。 dbx 把 aisphere-hub 里 100+ 处重复样板收敛成一套类型安全 API。 业务代码拿过去就能跑,而且用得特别好。

Documentation

Overview

Package dbx provides the unified, batteries-included database API for Aisphere Kernel.

dbx is the ONLY database abstraction that business code (handler / service / repository / worker) should depend on. Direct usage of `database/sql`, `gorm.io/gorm` (the *gorm.DB type), or driver-specific packages in business code is forbidden and will fail CI.

Design principle

dbx exposes a stable DB / Tx interface backed by GORM, with eight "pre-actions" built in so business code does not have to repeat the same boilerplate in every repository method:

  1. Error normalization — gorm.ErrRecordNotFound, pgconn.PgError 23505, mysql 1062 are automatically wrapped into dbx.ErrNoRows / dbx.ErrDuplicateKey so business code can use errors.Is without importing driver packages.
  2. Soft-delete safety — Unscoped queries require an explicit WithUnscoped(ctx) opt-in; accidental "select deleted rows" is blocked.
  3. OnConflict whitelist — SafeUpsert(model, row, allowedColumns) refuses to overwrite sensitive columns (owner_id / visibility / status / ...).
  4. Context propagation — DBFromContext(ctx) returns the request-scoped *gorm.DB (with tx / trace / request_id); InjectDB injects it. Repository methods stop repeating the same db(ctx) helper.
  5. Audit hook — BeforeCreate / AfterUpdate / BeforeDelete callbacks automatically record audit events via auditx; no per-method plumbing.
  6. Global QueryTimeout — Config.QueryTimeout applies to every query whose context lacks a deadline; no per-query WithTimeout boilerplate.
  7. Slow-query log — queries exceeding SlowQueryThreshold (default 200ms) are logged via logx with SQL text, duration, and caller.
  8. Metrics — every query is recorded as a Prometheus histogram with labels driver / operation / status; no manual instrumentation.

dbx does NOT depend on errorx (avoids cyclic dependency); business code converts dbx errors to errorx at the repository layer boundary.

30-second quickstart

import (
    "github.com/aisphereio/kernel/configx"
    "github.com/aisphereio/kernel/configx/file"
    "github.com/aisphereio/kernel/dbx"
    _ "github.com/aisphereio/kernel/dbx/postgres" // register "postgres" driver
)

var dbCfg dbx.Config
_ = cfg.Value("database").Scan(&dbCfg)

db, err := dbx.New(dbCfg)
if err != nil { return err }
defer db.Close()

// Create with auto audit + soft-delete + error normalization.
skill := &Skill{Name: "demo", OwnerID: "user_123"}
if err := db.Create(ctx, skill); err != nil {
    if errors.Is(err, dbx.ErrDuplicateKey) {
        return errorx.Conflict("AIHUB_SKILL_ALREADY_EXISTS", "技能已存在")
    }
    return errorx.Wrap(err, "AIHUB_SKILL_CREATE_FAILED", ...)
}

// Find one (auto filters soft-deleted rows).
var skill Skill
if err := db.FindOne(ctx, &skill, "name = ?", "demo"); err != nil {
    if errors.Is(err, dbx.ErrNoRows) {
        return errorx.NotFound("AIHUB_SKILL_NOT_FOUND", "技能不存在")
    }
    return errorx.Wrap(err, "AIHUB_SKILL_QUERY_FAILED", ...)
}

// Safe upsert: whitelist allowed columns; owner_id never overwritten.
if err := db.SafeUpsert(ctx, &skill, []string{"display_name", "version"}); err != nil { ... }

// Transaction with auto audit.
err = db.InTx(ctx, func(tx dbx.Tx) error {
    if err := tx.Create(ctx, skillRow); err != nil { return err }
    if err := tx.Create(ctx, versionRow); err != nil { return err }
    return nil // commit
})

Drivers

dbx does not import any driver by default. Import the driver subpackage to register it:

import _ "github.com/aisphereio/kernel/dbx/postgres"  // registers "postgres"
import _ "github.com/aisphereio/kernel/dbx/mysql"     // registers "mysql"

After import, set Config.Driver = "postgres" (or "mysql") and dbx picks the registered driver automatically.

Safe helpers

dbx provides semantic helpers that encode aisphere-hub's hard-won security lessons so they cannot be re-broken by a careless copy-paste:

db.SafeUpsert(ctx, row, allowedColumns)   // OnConflict + whitelist
db.WithUnscoped(ctx)                       // opt-in to read soft-deleted
db.AssertAffected(res, err)                // 0 rows → ErrNoRows
db.Increment(ctx, model, where, column, delta)  // atomic counter
db.Paginate(ctx, model, page, size, &out)  // limit+1 + hasMore + total

Forbidden patterns

Do not import `database/sql` in business code. Do not import driver packages (`gorm.io/driver/postgres`, `gorm.io/driver/mysql`) in business code. Do not import `gorm.io/gorm` directly in business code (use dbx.DB and dbx.Tx interfaces). Do not call Unscoped without WithUnscoped.

Documentation

See dbx/README.md for the user guide, docs/ai/dbx.md for the AI coding recipe, docs/contracts/dbx.md for behaviors that cannot change without a major version bump.

Index

Constants

View Source
const (
	CodeNoRows              = errorx.Code("DBX_NO_ROWS")
	CodeDuplicateKey        = errorx.Code("DBX_DUPLICATE_KEY")
	CodeTimeout             = errorx.Code("DBX_TIMEOUT")
	CodeSchemaNotReady      = errorx.Code("DBX_SCHEMA_NOT_READY")
	CodeDatabaseNotExist    = errorx.Code("DBX_DATABASE_NOT_EXIST")
	CodeForeignKeyViolation = errorx.Code("DBX_FOREIGN_KEY_VIOLATION")
	CodeClosed              = errorx.Code("DBX_CLOSED")
	CodeInvalidConfig       = errorx.Code("DBX_INVALID_CONFIG")
	CodeUnknownDriver       = errorx.Code("DBX_UNKNOWN_DRIVER")
	CodeTxStateInvalid      = errorx.Code("DBX_TX_STATE_INVALID")
	CodeUnsafeUpsert        = errorx.Code("DBX_UNSAFE_UPSERT")
	CodeNoEffect            = errorx.Code("DBX_NO_EFFECT")
	CodeOperationFailed     = errorx.Code("DBX_OPERATION_FAILED")
)

Variables

View Source
var (
	// ErrNoRows is returned by FindOne / FindOneByPK when no row matches.
	// It is a dbx-specific sentinel; GORM's gorm.ErrRecordNotFound is
	// automatically translated to this on every Find* call.
	ErrNoRows = newSentinel("dbx: no rows in result set")

	// ErrDuplicateKey is returned by Create / SafeUpsert when a unique
	// constraint is violated. Driver-specific detection:
	//   - postgres: pgconn.PgError Code == "23505"
	//   - mysql:    mysql.MySQLError Number == 1062
	ErrDuplicateKey = newSentinel("dbx: duplicate key")

	// ErrTimeout is returned when a query exceeds its context deadline or
	// the global QueryTimeout.
	ErrTimeout = newSentinel("dbx: query timed out")

	// ErrSchemaNotReady is returned when the underlying table does not
	// exist. This typically means migrations have not been applied.
	// Driver-specific detection:
	//   - postgres: pgconn.PgError Code == "42P01"
	//   - mysql:    mysql.MySQLError Number == 1146
	ErrSchemaNotReady = newSentinel("dbx: schema not ready (run migrations)")

	// ErrDatabaseNotExist is returned when the target database named in
	// the DSN does not exist on the server.
	// Driver-specific detection:
	//   - postgres: pgconn.PgError Code == "3D000" (invalid_catalog_name)
	//   - mysql:    mysql.MySQLError Number == 1049 (ER_BAD_DB_ERROR)
	//
	// When Config.AutoCreateDatabase is true, dbx.New traps this error
	// and auto-creates the database before retrying the connection.
	ErrDatabaseNotExist = newSentinel("dbx: target database does not exist")

	// ErrForeignKeyViolation is returned when a foreign-key constraint
	// is violated.
	//   - postgres: pgconn.PgError Code == "23503"
	//   - mysql:    mysql.MySQLError Number == 1452
	ErrForeignKeyViolation = newSentinel("dbx: foreign key violation")

	// ErrClosed is returned when a closed DB is used.
	ErrClosed = newSentinel("dbx: database is closed")

	// ErrNilConfig is returned by New when Config is missing required fields.
	ErrNilConfig = newSentinel("dbx: config is missing required fields")

	// ErrUnknownDriver is returned when the driver name has not been registered.
	ErrUnknownDriver = newSentinel("dbx: unknown driver (did you import dbx/postgres or dbx/mysql?)")

	// ErrTxRolledBack is returned when an operation is attempted on a rolled-back Tx.
	ErrTxRolledBack = newSentinel("dbx: transaction already rolled back")

	// ErrTxCommitted is returned when Commit is called twice on the same Tx.
	ErrTxCommitted = newSentinel("dbx: transaction already committed")

	// ErrUnscopedRequired is returned when a soft-delete-protected query
	// is attempted without WithUnscoped.
	ErrUnscopedRequired = newSentinel("dbx: query touches soft-deleted rows; use WithUnscoped to opt in")

	// ErrUnsafeUpsert is returned by SafeUpsert when the row contains a
	// protected column that is not in the allowedColumns whitelist.
	ErrUnsafeUpsert = newSentinel("dbx: SafeUpsert blocked a protected column")

	// ErrNoEffect is returned by AssertAffected when RowsAffected == 0.
	ErrNoEffect = newSentinel("dbx: operation affected 0 rows")

	// ErrNilDB is returned by helper functions when the canonical DB entrypoint is nil.
	ErrNilDB = newSentinel("dbx: nil DB")

	// ErrNilTxFunc is returned when a transaction helper receives a nil callback.
	ErrNilTxFunc = newSentinel("dbx: nil transaction function")
)

Functions

func AssertAffected

func AssertAffected(res *gorm.DB) error

AssertAffected returns ErrNoEffect if res.RowsAffected == 0, or wrapDriverErr(res.Error) otherwise. This is the "no row was changed" pattern from aisphere-hub skill.go (10+ occurrences of `if res.RowsAffected == 0 { return ErrXxxNotFound }`).

Usage:

res := tx.Model(&Skill{}).Where("name = ?", name).Updates(map[string]any{...})
if err := dbx.AssertAffected(res); err != nil {
    if errors.Is(err, dbx.ErrNoEffect) {
        return errorx.NotFound("AIHUB_SKILL_NOT_FOUND", ...)
    }
    return err
}

func ExecSQL added in v0.1.10

func ExecSQL(ctx context.Context, db DB, query string, args ...any) error

ExecSQL executes raw SQL through the canonical dbx entrypoint.

This is the supported direct-SQL escape hatch for generated model/RPC code and hot paths. It intentionally lives in dbx instead of a separate top-level sqlx package so Kernel has one database entrypoint.

func InSQLTx added in v0.1.10

func InSQLTx(ctx context.Context, db DB, fn func(txCtx context.Context) error) error

InSQLTx runs fn inside the canonical dbx transaction path. Raw SQL helpers called with the supplied txCtx automatically use the same transaction because dbx injects Tx into context.

func InjectDB

func InjectDB(ctx context.Context, gormDB *gorm.DB) context.Context

InjectDB attaches a *gorm.DB (typically a transaction-wrapped one) to ctx so that downstream DB.GORM(ctx) / FindOne(ctx, ...) / etc. pick it up automatically. This is the aisphere-hub Runtime.DBFromContext pattern encoded as a public API.

func InjectTx

func InjectTx(ctx context.Context, tx Tx) context.Context

InjectTx attaches a Tx to ctx so that downstream code can pick up the same transaction via DB.GORM(ctx) without explicit threading.

func IsDriverRegistered

func IsDriverRegistered(name string) bool

IsDriverRegistered returns true if the named driver has been registered.

func NormalizeError added in v0.0.5

func NormalizeError(err error) error

NormalizeError converts dbx sentinel/driver errors into Kernel errorx values while preserving the original error chain for errors.Is/errors.As callers.

func RegisterDriver

func RegisterDriver(name string, fn DriverOpener)

RegisterDriver registers a driver opener under the given name. Called by dbx/postgres and dbx/mysql in their init() functions. Panics if the same name is registered twice.

func RegisterErrorMapper

func RegisterErrorMapper(fn func(error) error)

RegisterErrorMapper adds a function that inspects an error and returns the matching dbx sentinel (or nil if not recognized). Called by driver subpackages via init().

func RegisteredDrivers

func RegisteredDrivers() []string

RegisteredDrivers returns the names of all registered drivers.

func RowSQL added in v0.1.10

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

RowSQL executes a raw SELECT expected to return one row.

func RowsSQL added in v0.1.10

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

RowsSQL executes a raw SELECT and returns database/sql rows. The caller must close the returned rows.

func ScanSQL added in v0.1.10

func ScanSQL(ctx context.Context, db DB, dest any, query string, args ...any) error

ScanSQL executes a raw SELECT and scans the result into dest.

func WithUnscoped

func WithUnscoped(ctx context.Context) context.Context

WithUnscoped returns a ctx that allows queries to read soft-deleted rows. Without this, any query on a model with DeletedAt returns an error if the caller tries to use Unscoped. This is the safety gate that prevents accidental "select deleted rows" leaks.

Types

type Config

type Config struct {
	// Driver selects the registered driver: "postgres" or "mysql".
	Driver string `json:"driver"`

	// DSN is the data source name (driver-specific connection string).
	DSN string `json:"dsn"`

	// MaxOpenConns limits the number of open connections. 0 means unlimited.
	MaxOpenConns int `json:"max_open_conns"`

	// MaxIdleConns limits the number of idle connections. 0 means default (2).
	MaxIdleConns int `json:"max_idle_conns"`

	// ConnMaxLifetime is how long a connection may live before being recycled.
	// Zero means no limit.
	ConnMaxLifetime time.Duration `json:"conn_max_lifetime_ns"`

	// ConnMaxIdleTime is how long an idle connection may live before being closed.
	// Zero means no limit.
	ConnMaxIdleTime time.Duration `json:"conn_max_idle_time_ns"`

	// QueryTimeout is the default timeout applied to queries when the context
	// has no deadline. Zero means no timeout (rely on context).
	QueryTimeout time.Duration `json:"query_timeout_ns"`

	// SlowQueryThreshold is the duration above which a query is logged as
	// slow via logx. Zero disables slow-query logging. Default 200ms.
	SlowQueryThreshold time.Duration `json:"slow_query_threshold_ns"`

	// AuditEnabled controls whether the auto audit hook is registered.
	// When true, BeforeCreate / AfterUpdate / BeforeDelete callbacks emit
	// audit events. Default false (opt-in to avoid surprises).
	AuditEnabled bool `json:"audit_enabled"`

	// MetricsEnabled controls whether per-query metrics are recorded.
	// Default false (opt-in).
	MetricsEnabled bool `json:"metrics_enabled"`

	// Logger is the component logger used for startup, slow-query and error logs.
	// If nil, dbx uses logx.DefaultLogger(), which should be initialized by kernel.New.
	Logger logx.Logger `json:"-" yaml:"-"`

	// Metrics is the optional metrics manager used when MetricsEnabled is true.
	Metrics metricsx.Manager `json:"-" yaml:"-"`

	// DryRun, when true, causes GORM to log SQL without executing it.
	// Useful for staging environment tests. Default false.
	DryRun bool `json:"dry_run"`

	// Debug, when true, logs every SQL statement via logx at Debug level.
	// Default false.
	Debug bool `json:"debug"`

	// AutoCreateDatabase, when true, causes dbx.New to create the target
	// database if it does not exist. This is useful for dev/test
	// environments where the operator does not want to pre-provision
	// databases manually.
	//
	// Default false. NEVER enable in production — automated database
	// creation bypasses standard provisioning workflows (IAM, backup
	// policies, schema migrations, monitoring) and should be handled by
	// infrastructure code (Terraform, Ansible, k8s Job, etc.).
	//
	// Implementation:
	//   - When gorm.Open fails with a "database does not exist" error
	//     (PG: SQLSTATE 3D000; MySQL: error 1049), the driver parses
	//     the DSN, connects to an admin DSN (PG: same host with
	//     dbname=postgres; MySQL: same DSN without the dbname segment),
	//     issues CREATE DATABASE <name>, then retries the original
	//     gorm.Open.
	//   - The CREATE DATABASE uses the cluster's default encoding/ctype.
	//     For custom encoding, set up the database out-of-band and leave
	//     this field false.
	//   - The database name is quoted to prevent SQL injection via the
	//     DSN, but the DSN itself is trusted (it comes from config, not
	//     user input).
	//   - Errors during admin connect or CREATE DATABASE are returned
	//     wrapped, so the caller sees the original "database does not
	//     exist" error plus the auto-create failure context.
	AutoCreateDatabase bool `json:"auto_create_database"`
}

Config holds the configuration for a DB connection pool.

Fields use json tags so the struct can be loaded directly from configx via `cfg.Value("database").Scan(&dbCfg)`.

func (Config) Validate

func (c Config) Validate() error

Validate returns ErrNilConfig if required fields are missing.

type DB

type DB interface {
	// GORM exposes the underlying *gorm.DB for advanced cases (Preload,
	// raw SQL, complex Where chains). Business code SHOULD prefer the
	// typed helpers below; GORM is the escape hatch.
	GORM(ctx context.Context) *gorm.DB

	// FindOne executes a query that returns at most one row, scanning
	// the result into dest (a pointer to a struct). Returns ErrNoRows
	// if no row matches.
	FindOne(ctx context.Context, dest any, query any, args ...any) error

	// FindOneByPK finds a row by primary key. pk may be a single value
	// or a map for composite keys.
	FindOneByPK(ctx context.Context, dest any, pk any) error

	// FindMany executes a query that returns multiple rows, scanning
	// each into dest (a pointer to a slice of structs).
	FindMany(ctx context.Context, dest any, query any, args ...any) error

	// Count returns the number of rows matching the query.
	Count(ctx context.Context, model any, query any, args ...any) (int64, error)

	// Create inserts one or more rows. If dest is a struct, it is updated
	// with the auto-generated primary key (if any).
	Create(ctx context.Context, dest any) error

	// Save updates the row by primary key, creating it if it does not exist.
	// All fields are written; use Update for partial updates.
	Save(ctx context.Context, dest any) error

	// Update updates specific columns by query.
	//   db.Update(ctx, &Skill{}, "name = ?", "demo", map[string]any{"status":"online"})
	Update(ctx context.Context, model any, query any, args []any, columns map[string]any) error

	// UpdateColumns is like Update but uses GORM's UpdateColumns (skips
	// autoUpdateTime hooks). Use for atomic increments via gorm.Expr.
	UpdateColumns(ctx context.Context, model any, query any, args []any, columns map[string]any) error

	// Delete soft-deletes rows matching the query (or hard-deletes if the
	// model has no DeletedAt field).
	Delete(ctx context.Context, model any, query any, args ...any) error

	// DeleteByPK soft-deletes (or hard-deletes) a row by primary key.
	DeleteByPK(ctx context.Context, model any, pk any) error

	// SafeUpsert performs an INSERT ... ON CONFLICT DO UPDATE (postgres) or
	// INSERT ... ON DUPLICATE KEY UPDATE (mysql). Only columns listed in
	// allowedColumns are updated on conflict; other columns (including
	// owner_id / visibility / status and other sensitive fields) are
	// never overwritten. This is the SECURITY-annotated pattern from
	// aisphere-hub skill.go.
	SafeUpsert(ctx context.Context, dest any, allowedColumns []string) error

	// Increment atomically increments a column by delta. Implemented as
	// UpdateColumns(col = col + delta), server-side, race-free.
	Increment(ctx context.Context, model any, query any, args []any, column string, delta int64) error

	// Paginate returns a page of rows + total count + hasMore.
	// page is 1-indexed; size is capped at 100.
	Paginate(ctx context.Context, dest any, model any, query any, args []any, page, size int) (*PageResult, error)

	// InTx runs fn inside a transaction. If fn returns nil, the
	// transaction is committed. If fn returns a non-nil error or panics,
	// the transaction is rolled back and the error (or recovered panic)
	// is returned.
	InTx(ctx context.Context, fn func(Tx) error) error

	// BeginTx starts a transaction. Manual commit/rollback; prefer InTx
	// unless you need finer control.
	BeginTx(ctx context.Context) (Tx, error)

	// PingContext verifies the database is reachable.
	PingContext(ctx context.Context) error

	// AutoMigrate creates or updates tables for the given models. Safe to
	// call repeatedly (idempotent). NOT recommended for production — use
	// SQL migrations for schema changes in prod.
	AutoMigrate(ctx context.Context, models ...any) error

	// Tables returns the list of tables in the current schema.
	Tables(ctx context.Context) ([]string, error)

	// Stats returns connection pool statistics.
	Stats() DBStats

	// DriverName returns the registered driver name ("postgres" / "mysql").
	DriverName() string

	// Close closes the database, releasing all connections. Idempotent.
	Close() error
}

DB is the runtime database interface used by kernel modules and apps.

All methods accept context.Context. Methods that take a model pointer (FindOne / Create / Save / SafeUpsert / Delete) operate on GORM models with the same struct tag conventions as GORM (`gorm:"column:..."`).

func New

func New(cfg Config) (DB, error)

New opens a database connection using the supplied Config.

The driver must have been registered by importing dbx/postgres or dbx/mysql. Returns ErrUnknownDriver if not registered, ErrNilConfig if Driver or DSN is missing.

The returned DB has all eight pre-actions wired up: error normalization, soft-delete safety, OnConflict whitelist, context propagation, audit hook (if Config.AuditEnabled), global QueryTimeout, slow-query log, and metrics (if Config.MetricsEnabled).

type DBStats

type DBStats = sql.DBStats

DBStats mirrors gorm's underlying stats. We use sql.DBStats to avoid exporting gorm internals in the public API.

type DriverOpener

type DriverOpener func(dsn string, cfg Config) (*gorm.DB, error)

DriverOpener opens a *gorm.DB for the given DSN. Driver subpackages register their opener via RegisterDriver.

type PageResult

type PageResult struct {
	Items   []any
	Total   int64
	Page    int
	Size    int
	HasMore bool
}

PageResult holds a paginated query result.

type Tx

type Tx interface {
	GORM(ctx context.Context) *gorm.DB
	FindOne(ctx context.Context, dest any, query any, args ...any) error
	FindOneByPK(ctx context.Context, dest any, pk any) error
	FindMany(ctx context.Context, dest any, query any, args ...any) error
	Count(ctx context.Context, model any, query any, args ...any) (int64, error)
	Create(ctx context.Context, dest any) error
	Save(ctx context.Context, dest any) error
	Update(ctx context.Context, model any, query any, args []any, columns map[string]any) error
	UpdateColumns(ctx context.Context, model any, query any, args []any, columns map[string]any) error
	Delete(ctx context.Context, model any, query any, args ...any) error
	DeleteByPK(ctx context.Context, model any, pk any) error
	SafeUpsert(ctx context.Context, dest any, allowedColumns []string) error
	Increment(ctx context.Context, model any, query any, args []any, column string, delta int64) error
	Commit() error
	Rollback() error
}

Tx is a database transaction. All methods behave like the corresponding DB methods but operate within the transaction.

Directories

Path Synopsis
Package mysql registers the "mysql" driver for dbx.
Package mysql registers the "mysql" driver for dbx.
Package postgres registers the "postgres" driver for dbx.
Package postgres registers the "postgres" driver for dbx.

Jump to

Keyboard shortcuts

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