Raw SQL power. Zero boilerplate. One API for any database.
Stop writing the same transaction management code over and over. Octobe gives you clean, testable database handlers with automatic transaction lifecycle management.
Without Octobe - Messy, repetitive, error-prone:
func CreateUser(db *sql.DB, name string) (*User, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
}
defer func() {
if err != nil {
tx.Rollback()
return
}
tx.Commit()
}()
var user User
err = tx.QueryRow("INSERT INTO users (name) VALUES ($1) RETURNING id, name", name).
Scan(&user.ID, &user.Name)
return &user, err
}With Octobe - Clean, structured, automatic:
func CreateUser(name string) octobe.Handler[User] {
return func(builder postgres.Builder) (User, error) {
var user User
query := builder(`INSERT INTO users (name) VALUES ($1) RETURNING id, name`)
err := query.Arguments(name).QueryRow(&user.ID, &user.Name)
return user, err
}
}
// Usage - transaction management is automatic
user, err := octobe.Execute(session, CreateUser("Alice"))âś… Zero boilerplate - No more manual transaction begin/commit/rollback
âś… Raw SQL freedom - Write the queries you want, not what an ORM allows
âś… Built for testing - Mock any database interaction with ease
âś… Database agnostic - One API for PostgreSQL and more (coming soon)
âś… Production ready - Handle panics, errors, and edge cases automatically
Install:
go get github.com/Kansuler/octobe/v3Use:
// 1. Create handlers (your SQL logic)
func GetProduct(id int) octobe.Handler[Product] {
return func(builder postgres.Builder) (Product, error) {
var p Product
query := builder(`SELECT id, name FROM products WHERE id = $1`)
err := query.Arguments(id).QueryRow(&p.ID, &p.Name)
return p, err
}
}
// 2. Execute with automatic transaction management
db, _ := octobe.New(postgres.OpenPGXPool(ctx, dsn))
err := db.StartTransaction(ctx, func(session octobe.BuilderSession[postgres.Builder]) error {
product, err := octobe.Execute(session, GetProduct(123))
if err != nil {
return err // Automatic rollback
}
fmt.Printf("Product: %+v\n", product)
return nil // Automatic commit
})That's it. No manual transaction management, no connection handling, no boilerplate.
Here's a complete example showing the handler pattern in action:
package main
import (
"context"
"fmt"
"os"
"github.com/Kansuler/octobe"
"github.com/Kansuler/octobe/driver/postgres"
)
type Product struct {
ID int
Name string
}
// Handlers are pure functions that encapsulate SQL logic
func CreateProduct(name string) octobe.Handler[Product] {
return func(builder postgres.Builder) (Product, error) {
var product Product
query := builder(`INSERT INTO products (name) VALUES ($1) RETURNING id, name`)
err := query.Arguments(name).QueryRow(&product.ID, &product.Name)
return product, err
}
}
func GetProduct(id int) octobe.Handler[Product] {
return func(builder postgres.Builder) (Product, error) {
var product Product
query := builder(`SELECT id, name FROM products WHERE id = $1`)
err := query.Arguments(id).QueryRow(&product.ID, &product.Name)
return product, err
}
}
func main() {
ctx := context.Background()
db, err := octobe.New(postgres.OpenPGXPool(ctx, os.Getenv("DSN")))
if err != nil {
panic(err)
}
defer db.Close(ctx)
// Everything happens in one transaction - automatic begin/commit/rollback
err = db.StartTransaction(ctx, func(session octobe.BuilderSession[postgres.Builder]) error {
// Create product
product, err := octobe.Execute(session, CreateProduct("Super Widget"))
if err != nil {
return err // Automatic rollback on any error
}
// Fetch it back
fetched, err := octobe.Execute(session, GetProduct(product.ID))
if err != nil {
return err
}
fmt.Printf("Created and fetched: %+v\n", fetched)
return nil // Automatic commit
})
if err != nil {
panic(err)
}
// Or do it without a wrapper function, WithPGXTxOptions starts a transaction.
session, err := db.Begin(ctx, postgres.WithPGXTxOptions(postgres.PGXTxOptions{}))
if err != nil {
panic(err)
}
defer session.Rollback() // Safe to call, will be a no-op if already committed
product, err := octobe.Execute(session, CreateProduct("Another Widget"))
if err != nil {
panic(err)
}
// Create product
product, err := octobe.Execute(session, CreateProduct("Super Widget"))
if err != nil {
panic(err)
}
// Fetch it back
fetched, err := octobe.Execute(session, GetProduct(product.ID))
if err != nil {
return err
}
fmt.Printf("Created and fetched: %+v\n", fetched)
return session.Commit() // Explicit commit
}Mock any handler without touching your database:
func TestCreateProduct(t *testing.T) {
ctx := context.Background()
// 1. Create mock
mockPool := mock.NewPGXPoolMock()
db, _ := octobe.New(postgres.OpenPGXPoolWithPool(mockPool))
// 2. Set expectations
rows := mock.NewMockRow(1, "Super Widget")
mockPool.ExpectQueryRow("INSERT INTO products").WithArgs("Super Widget").WillReturnRow(rows)
// 3. Test your handler
session, _ := db.Begin(ctx)
product, err := octobe.Execute(session, CreateProduct("Super Widget"))
// 4. Assert results
require.NoError(t, err)
require.Equal(t, 1, product.ID)
require.NoError(t, mockPool.AllExpectationsMet())
}Before (database/sql):
func GetUser(db *sql.DB, id int) (*User, error) {
tx, err := db.Begin()
if err != nil {
return nil, err
}
defer func() {
if err != nil {
tx.Rollback()
return
}
tx.Commit()
}()
var user User
err = tx.QueryRow("SELECT id, name FROM users WHERE id = ?", id).
Scan(&user.ID, &user.Name)
return &user, err
}After (Octobe):
func GetUser(id int) octobe.Handler[User] {
return func(builder postgres.Builder) (User, error) {
var user User
err := builder(`SELECT id, name FROM users WHERE id = $1`).
Arguments(id).
QueryRow(&user.ID, &user.Name)
return user, err
}
}
// Usage
user, err := octobe.Execute(session, GetUser(123))
// Or with automatic transaction management:
var user User
err := db.StartTransaction(ctx, func(session octobe.BuilderSession[postgres.Builder]) error {
user, err := octobe.Execute(session, GetUser(123))
return err
})Before (GORM):
type User struct {
ID uint `gorm:"primaryKey"`
Name string
}
func GetUserWithPosts(db *gorm.DB, userID uint) (User, []Post, error) {
var user User
var posts []Post
err := db.First(&user, userID).Error
if err != nil {
return user, posts, err
}
err = db.Where("user_id = ?", userID).Find(&posts).Error
return user, posts, err
}After (Octobe):
type UserWithPosts struct {
User User
Posts []Post
}
func PostsByUserID(userID int) octobe.Handler[UserWithPosts] {
return func(builder postgres.Builder) (UserWithPosts, error) {
var result UserWithPosts
query := builder(`
SELECT
u.id, u.name,
p.id, p.title, p.content
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE u.id = $1
`)
err := query.Arguments(userID).Query(func(rows postgres.Rows) error {
for rows.Next() {
var post Post
err := rows.Scan(&result.User.ID, &result.User.Name, &post.ID, &post.Title, &post.Content)
if err != nil {
return err
}
result.Posts = append(result.Posts, post)
}
return nil
})
return result, err
}
}Before (Squirrel):
func UpdateUser(db *sql.DB, id int, name string) error {
sql, args, err := squirrel.
Update("users").
Set("name", name).
Where(squirrel.Eq{"id": id}).
PlaceholderFormat(squirrel.Dollar).
ToSql()
if err != nil {
return err
}
_, err = db.Exec(sql, args...)
return err
}After (Octobe):
func UpdateUser(id int, name string) octobe.Handler[octobe.Void] {
return func(builder postgres.Builder) (octobe.Void, error) {
query := builder(`UPDATE users SET name = $1 WHERE id = $2`)
_, err := query.Arguments(name, id).Exec()
return nil, err
}
}Octobe uses the underlying driver's connection pooling (like pgxpool). Configure your pool settings when creating the driver:
config, _ := pgxpool.ParseConfig(dsn)
config.MaxConns = 50
pool, _ := pgxpool.NewWithConfig(ctx, config)
db, _ := octobe.New(postgres.OpenPGXWithPool(pool))# Core package
go get github.com/Kansuler/octobe/v3
# Database drivers
go get github.com/Kansuler/octobe/v3/driver/postgres- PostgreSQL: Full-featured driver using pgx/v5
- SQLite: Coming soon
- Clickhouse: Coming soon
Want to add a driver? Check our Driver Development Guide.
Check out the examples directory for complete, runnable examples:
- Simple CRUD: Basic operations to get started
- Blog Application: Complex real-world example with relationships
We welcome contributions! Here's how to get started:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Run tests:
docker compose up --abort-on-container-exit - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Creating a new database driver? Follow these steps:
- Implement the core interfaces in
driver/yourdb/ - Add comprehensive tests
- Create mock implementations for testing
- Add examples and documentation
- Submit a PR with benchmarks
See the PostgreSQL driver as a reference implementation.
MIT License - see the LICENSE file for details.