# BUN ORM Integration Guide ## Overview BUN is a fast and lightweight SQL-first ORM for Go. For WhatsHooked Phase 2, we use BUN with PostgreSQL and SQLite, integrated with ResolveSpec for REST API generation. Official Documentation: https://bun.uptrace.dev/ ## Installation ```bash go get github.com/uptrace/bun go get github.com/uptrace/bun/driver/pgdriver # PostgreSQL go get github.com/uptrace/bun/driver/sqliteshim # SQLite go get github.com/uptrace/bun/dialect/pgdialect go get github.com/uptrace/bun/dialect/sqlitedialect ``` ## Setup ### Database Connection ```go import ( "database/sql" "github.com/uptrace/bun" "github.com/uptrace/bun/driver/pgdriver" "github.com/uptrace/bun/dialect/pgdialect" ) // PostgreSQL func NewPostgresDB(dsn string) *bun.DB { sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn))) db := bun.NewDB(sqldb, pgdialect.New()) return db } // SQLite import ( "github.com/uptrace/bun/driver/sqliteshim" "github.com/uptrace/bun/dialect/sqlitedialect" ) func NewSQLiteDB(path string) *bun.DB { sqldb, _ := sql.Open(sqliteshim.ShimName, path) db := bun.NewDB(sqldb, sqlitedialect.New()) return db } ``` ### Model Definition BUN models use struct tags: ```go type User struct { bun.BaseModel `bun:"table:users,alias:u"` ID string `bun:"id,pk,type:varchar(36)"` Username string `bun:"username,unique,notnull"` Email string `bun:"email,unique,notnull"` Password string `bun:"password,notnull"` Role string `bun:"role,notnull,default:'user'"` Active bool `bun:"active,notnull,default:true"` CreatedAt time.Time `bun:"created_at,notnull,default:now()"` UpdatedAt time.Time `bun:"updated_at,notnull,default:now()"` DeletedAt bun.NullTime `bun:"deleted_at,soft_delete"` // Relationships APIKeys []*APIKey `bun:"rel:has-many,join:id=user_id"` } ``` ## CRUD Operations ### Create ```go user := &User{ ID: uuid.New().String(), Username: "john", Email: "john@example.com", Password: hashedPassword, Role: "user", Active: true, } _, err := db.NewInsert(). Model(user). Exec(ctx) ``` ### Read ```go // Single record user := new(User) err := db.NewSelect(). Model(user). Where("id = ?", userID). Scan(ctx) // Multiple records var users []User err := db.NewSelect(). Model(&users). Where("active = ?", true). Order("created_at DESC"). Limit(10). Scan(ctx) ``` ### Update ```go // Update specific fields _, err := db.NewUpdate(). Model(&user). Column("username", "email"). Where("id = ?", user.ID). Exec(ctx) // Update all fields _, err := db.NewUpdate(). Model(&user). WherePK(). Exec(ctx) ``` ### Delete ```go // Soft delete (if model has soft_delete tag) _, err := db.NewDelete(). Model(&user). Where("id = ?", userID). Exec(ctx) // Hard delete _, err := db.NewDelete(). Model(&user). Where("id = ?", userID). ForceDelete(). Exec(ctx) ``` ## Relationships ### Has-Many ```go type User struct { bun.BaseModel `bun:"table:users"` ID string `bun:"id,pk"` APIKeys []*APIKey `bun:"rel:has-many,join:id=user_id"` } type APIKey struct { bun.BaseModel `bun:"table:api_keys"` ID string `bun:"id,pk"` UserID string `bun:"user_id,notnull"` User *User `bun:"rel:belongs-to,join:user_id=id"` } // Load with relations var users []User err := db.NewSelect(). Model(&users). Relation("APIKeys"). Scan(ctx) ``` ### Belongs-To ```go var apiKey APIKey err := db.NewSelect(). Model(&apiKey). Relation("User"). Where("api_key.id = ?", keyID). Scan(ctx) ``` ### Many-to-Many ```go type User struct { ID string `bun:"id,pk"` Roles []*Role `bun:"m2m:user_roles,join:User=Role"` } type Role struct { ID string `bun:"id,pk"` Users []*User `bun:"m2m:user_roles,join:Role=User"` } type UserRole struct { UserID string `bun:"user_id,pk"` RoleID string `bun:"role_id,pk"` User *User `bun:"rel:belongs-to,join:user_id=id"` Role *Role `bun:"rel:belongs-to,join:role_id=id"` } ``` ## Queries ### Filtering ```go // WHERE clauses err := db.NewSelect(). Model(&users). Where("role = ?", "admin"). Where("active = ?", true). Scan(ctx) // OR conditions err := db.NewSelect(). Model(&users). WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery { return q. Where("role = ?", "admin"). Where("role = ?", "moderator") }). Scan(ctx) // IN clause err := db.NewSelect(). Model(&users). Where("role IN (?)", bun.In([]string{"admin", "moderator"})). Scan(ctx) // LIKE err := db.NewSelect(). Model(&users). Where("username LIKE ?", "john%"). Scan(ctx) ``` ### Sorting ```go err := db.NewSelect(). Model(&users). Order("created_at DESC"). Order("username ASC"). Scan(ctx) ``` ### Pagination ```go // Offset/Limit err := db.NewSelect(). Model(&users). Limit(20). Offset(40). Scan(ctx) // Count count, err := db.NewSelect(). Model((*User)(nil)). Where("active = ?", true). Count(ctx) ``` ### Aggregations ```go // COUNT count, err := db.NewSelect(). Model((*User)(nil)). Count(ctx) // GROUP BY type Result struct { Role string `bun:"role"` Count int `bun:"count"` } var results []Result err := db.NewSelect(). Model((*User)(nil)). Column("role"). ColumnExpr("COUNT(*) as count"). Group("role"). Scan(ctx, &results) ``` ## Transactions ```go err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { // Create user _, err := tx.NewInsert(). Model(&user). Exec(ctx) if err != nil { return err } // Create API key _, err = tx.NewInsert(). Model(&apiKey). Exec(ctx) if err != nil { return err } return nil }) ``` ## Migrations ### Create Table ```go _, err := db.NewCreateTable(). Model((*User)(nil)). IfNotExists(). Exec(ctx) ``` ### Add Column ```go _, err := db.NewAddColumn(). Model((*User)(nil)). ColumnExpr("phone VARCHAR(50)"). Exec(ctx) ``` ### Drop Table ```go _, err := db.NewDropTable(). Model((*User)(nil)). IfExists(). Exec(ctx) ``` ## Hooks ### Before/After Hooks ```go var _ bun.BeforeAppendModelHook = (*User)(nil) func (u *User) BeforeAppendModel(ctx context.Context, query bun.Query) error { switch query.(type) { case *bun.InsertQuery: u.CreatedAt = time.Now() case *bun.UpdateQuery: u.UpdatedAt = time.Now() } return nil } ``` ## ResolveSpec Integration ### Setup with BUN ```go import ( "github.com/bitechdev/ResolveSpec/pkg/restheadspec" "git.warky.dev/wdevs/whatshooked/pkg/models" ) // Create ResolveSpec handler with BUN handler := restheadspec.NewHandlerWithBun(db) // Setup routes (models are automatically discovered) router := mux.NewRouter() restheadspec.SetupMuxRoutes(router, handler, nil) ``` ### Custom Queries ```go // Use BUN directly for complex queries var results []struct { UserID string HookCount int } err := db.NewSelect(). Model((*models.Hook)(nil)). Column("user_id"). ColumnExpr("COUNT(*) as hook_count"). Group("user_id"). Scan(ctx, &results) ``` ## Performance Tips ### 1. Use Column Selection ```go // Don't load unnecessary columns err := db.NewSelect(). Model(&users). Column("id", "username", "email"). Scan(ctx) ``` ### 2. Batch Operations ```go // Insert multiple records _, err := db.NewInsert(). Model(&users). Exec(ctx) ``` ### 3. Use Indexes ```dbml indexes { (user_id) [name: 'idx_hooks_user_id'] (created_at) [name: 'idx_hooks_created_at'] } ``` ### 4. Connection Pooling ```go sqldb.SetMaxOpenConns(25) sqldb.SetMaxIdleConns(25) sqldb.SetConnMaxLifetime(5 * time.Minute) ``` ### 5. Prepared Statements BUN automatically uses prepared statements for better performance. ## Common Patterns ### Repository Pattern ```go type UserRepository struct { db *bun.DB } func NewUserRepository(db *bun.DB) *UserRepository { return &UserRepository{db: db} } func (r *UserRepository) GetByID(ctx context.Context, id string) (*models.User, error) { user := new(models.User) err := r.db.NewSelect(). Model(user). Where("id = ?", id). Scan(ctx) return user, err } func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*models.User, error) { user := new(models.User) err := r.db.NewSelect(). Model(user). Where("username = ?", username). Scan(ctx) return user, err } ``` ### Soft Deletes ```go type User struct { bun.BaseModel `bun:"table:users"` ID string `bun:"id,pk"` DeletedAt bun.NullTime `bun:"deleted_at,soft_delete"` } // Automatically filters out soft-deleted records var users []User err := db.NewSelect(). Model(&users). Scan(ctx) // Include soft-deleted records err := db.NewSelect(). Model(&users). WhereAllWithDeleted(). Scan(ctx) ``` ### Multi-Tenancy ```go // Filter by user_id for all queries err := db.NewSelect(). Model(&hooks). Where("user_id = ?", currentUserID). Scan(ctx) ``` ## Error Handling ```go import "github.com/uptrace/bun/driver/pgdriver" err := db.NewSelect().Model(&user).Where("id = ?", id).Scan(ctx) switch { case errors.Is(err, sql.ErrNoRows): // Not found case err != nil: var pgErr pgdriver.Error if errors.As(err, &pgErr) { // PostgreSQL specific error if pgErr.IntegrityViolation() { // Handle constraint violation } } } ``` ## Testing ```go import ( "github.com/uptrace/bun" "github.com/uptrace/bun/dbfixture" ) func TestUserRepository(t *testing.T) { // Use in-memory SQLite for tests db := NewSQLiteDB(":memory:") defer db.Close() // Create tables ctx := context.Background() _, err := db.NewCreateTable(). Model((*models.User)(nil)). Exec(ctx) require.NoError(t, err) // Test repository repo := NewUserRepository(db) user := &models.User{...} err = repo.Create(ctx, user) require.NoError(t, err) } ``` ## References - BUN Documentation: https://bun.uptrace.dev/ - BUN GitHub: https://github.com/uptrace/bun - PostgreSQL Driver: https://bun.uptrace.dev/postgres/ - SQLite Driver: https://bun.uptrace.dev/sqlite/ - ResolveSpec: https://github.com/bitechdev/ResolveSpec