Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 74 additions & 77 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func CreateUser(db *sql.DB, name string) (*User, error) {
**With Octobe** - Clean, structured, automatic:

```go
func CreateUser(name string) postgres.Handler[User] {
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`)
Expand All @@ -49,7 +49,7 @@ func CreateUser(name string) postgres.Handler[User] {
}

// Usage - transaction management is automatic
user, err := postgres.Execute(session, CreateUser("Alice"))
user, err := octobe.Execute(session, CreateUser("Alice"))
```

## Why Octobe?
Expand All @@ -60,6 +60,8 @@ user, err := postgres.Execute(session, CreateUser("Alice"))

✅ **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

## Quick Start
Expand All @@ -74,7 +76,7 @@ Use:

```go
// 1. Create handlers (your SQL logic)
func GetProduct(id int) postgres.Handler[Product] {
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`)
Expand All @@ -86,7 +88,7 @@ func GetProduct(id int) postgres.Handler[Product] {
// 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 := postgres.Execute(session, GetProduct(123))
product, err := octobe.Execute(session, GetProduct(123))
if err != nil {
return err // Automatic rollback
}
Expand Down Expand Up @@ -119,7 +121,7 @@ type Product struct {
}

// Handlers are pure functions that encapsulate SQL logic
func CreateProduct(name string) postgres.Handler[Product] {
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`)
Expand All @@ -128,7 +130,7 @@ func CreateProduct(name string) postgres.Handler[Product] {
}
}

func GetProduct(id int) postgres.Handler[Product] {
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`)
Expand All @@ -148,13 +150,13 @@ func main() {
// Everything happens in one transaction - automatic begin/commit/rollback
err = db.StartTransaction(ctx, func(session octobe.BuilderSession[postgres.Builder]) error {
// Create product
product, err := postgres.Execute(session, CreateProduct("Super Widget"))
product, err := octobe.Execute(session, CreateProduct("Super Widget"))
if err != nil {
return err // Automatic rollback on any error
}

// Fetch it back
fetched, err := postgres.Execute(session, GetProduct(product.ID))
fetched, err := octobe.Execute(session, GetProduct(product.ID))
if err != nil {
return err
}
Expand All @@ -166,6 +168,35 @@ func main() {
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)
}
Comment on lines +181 to +184
Copy link

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code block duplicates the product creation from lines 181-184. The variable product is being reassigned without using the first created product, which could be confusing.

Suggested change
product, err := octobe.Execute(session, CreateProduct("Another Widget"))
if err != nil {
panic(err)
}

Copilot uses AI. Check for mistakes.

// 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
Copy link

Copilot AI Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return statement should use session.Commit() to be consistent with the comment on line 199 and the function structure. Currently it returns the error from octobe.Execute instead of committing the transaction.

Copilot uses AI. Check for mistakes.
}

fmt.Printf("Created and fetched: %+v\n", fetched)
return session.Commit() // Explicit commit
}
```

Expand All @@ -187,7 +218,7 @@ func TestCreateProduct(t *testing.T) {

// 3. Test your handler
session, _ := db.Begin(ctx)
product, err := postgres.Execute(session, CreateProduct("Super Widget"))
product, err := octobe.Execute(session, CreateProduct("Super Widget"))

// 4. Assert results
require.NoError(t, err)
Expand Down Expand Up @@ -226,7 +257,7 @@ func GetUser(db *sql.DB, id int) (*User, error) {
**After (Octobe):**

```go
func GetUser(id int) postgres.Handler[User] {
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`).
Expand All @@ -237,11 +268,11 @@ func GetUser(id int) postgres.Handler[User] {
}

// Usage
user, err := postgres.Execute(session, GetUser(123))
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 := postgres.Execute(session, GetUser(123))
user, err := octobe.Execute(session, GetUser(123))
return err
})
```
Expand Down Expand Up @@ -273,32 +304,35 @@ func GetUserWithPosts(db *gorm.DB, userID uint) (User, []Post, error) {
**After (Octobe):**

```go
func GetUserWithPosts(userID int) postgres.Handler[UserWithPosts] {
return func(builder postgres.Builder) (UserWithPosts, error) {
var result UserWithPosts

// Get user
userQuery := builder(`SELECT id, name FROM users WHERE id = $1`)
err := userQuery.Arguments(userID).QueryRow(&result.User.ID, &result.User.Name)
if err != nil {
return result, err
}
type UserWithPosts struct {
User User
Posts []Post
}

// Get posts
postsQuery := builder(`SELECT id, title, content FROM posts WHERE user_id = $1`)
err = postsQuery.Arguments(userID).Query(func(rows postgres.Rows) error {
for rows.Next() {
var post Post
if err := rows.Scan(&post.ID, &post.Title, &post.Content); err != nil {
return err
}
result.Posts = append(result.Posts, post)
}
return rows.Err()
})

return result, err
}
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
}
}
```

Expand Down Expand Up @@ -326,7 +360,7 @@ func UpdateUser(db *sql.DB, id int, name string) error {
**After (Octobe):**

```go
func UpdateUser(id int, name string) postgres.Handler[octobe.Void] {
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()
Expand All @@ -343,35 +377,7 @@ Octobe uses the underlying driver's connection pooling (like pgxpool). Configure
config, _ := pgxpool.ParseConfig(dsn)
config.MaxConns = 50
pool, _ := pgxpool.NewWithConfig(ctx, config)
db, _ := octobe.New(postgres.OpenPGXPoolWithPool(pool))
```

### Can I use Octobe with existing database code?

Yes! Octobe doesn't require you to rewrite everything. You can:

1. Use Octobe for new features
2. Gradually migrate existing code
3. Use both approaches in the same application
4. Extract the underlying connection for direct use when needed

### What about logging and observability?

Add logging middleware to your handlers:

```go
func WithLogging[T any](name string, handler postgres.Handler[T]) postgres.Handler[T] {
return func(builder postgres.Builder) (T, error) {
start := time.Now()
result, err := handler(builder)

log.Printf("Handler %s took %v, error: %v", name, time.Since(start), err)
return result, err
}
}

// Usage
user, err := postgres.Execute(session, WithLogging("GetUser", GetUser(123)))
db, _ := octobe.New(postgres.OpenPGXWithPool(pool))
```

## Installation & Drivers
Expand All @@ -388,8 +394,7 @@ go get github.com/Kansuler/octobe/v3/driver/postgres

- **PostgreSQL**: Full-featured driver using pgx/v5
- **SQLite**: _Coming soon_
- **MySQL**: _Coming soon_
- **SQL Server**: _Coming soon_
- **Clickhouse**: _Coming soon_

Want to add a driver? Check our [Driver Development Guide](CONTRIBUTING.md#driver-development).

Expand All @@ -413,14 +418,6 @@ We welcome contributions! Here's how to get started:
5. Push to the branch (`git push origin feature/amazing-feature`)
6. Open a Pull Request

### Development Areas

- **New Database Drivers**: SQLite, MySQL, SQL Server
- **Testing Utilities**: More helper functions and assertions
- **Documentation**: Examples, guides, and API documentation
- **Performance**: Benchmarks and optimizations
- **Tooling**: Code generation, CLI tools, IDE plugins

### Driver Development

Creating a new database driver? Follow these steps:
Expand Down
12 changes: 6 additions & 6 deletions octobe.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// }
//
// err = db.StartTransaction(ctx, func(session octobe.BuilderSession[postgres.Builder]) error {
// user, err := postgres.Execute(session, CreateUser("Alice"))
// user, err := octobe.Execute(session, CreateUser("Alice"))
// return err // Automatic rollback on error, commit on success
// })
package octobe
Expand Down Expand Up @@ -127,7 +127,7 @@ type BuilderSession[BUILDER any] interface {
//
// Example:
//
// func DeleteUser(id int) postgres.Handler[octobe.Void] {
// func DeleteUser(id int) octobe.Handler[octobe.Void] {
// return func(builder postgres.Builder) (octobe.Void, error) {
// query := builder(`DELETE FROM users WHERE id = $1`)
// _, err := query.Arguments(id).Exec()
Expand All @@ -151,12 +151,12 @@ type Void *struct{}
// Example:
//
// err := db.StartTransaction(ctx, func(session octobe.BuilderSession[postgres.Builder]) error {
// user, err := postgres.Execute(session, CreateUser("Alice"))
// user, err := octobe.Execute(session, CreateUser("Alice"))
// if err != nil {
// return err // Automatic rollback
// }
//
// _, err = postgres.Execute(session, CreateProfile(user.ID))
// _, err = octobe.Execute(session, CreateProfile(user.ID))
// return err // Automatic commit if nil, rollback if error
// })
func StartTransaction[DRIVER, CONFIG, BUILDER any](ctx context.Context, driver Driver[DRIVER, CONFIG, BUILDER], fn func(session BuilderSession[BUILDER]) error, opts ...Option[CONFIG]) (err error) {
Expand Down Expand Up @@ -214,7 +214,7 @@ func Execute[RESULT, BUILDER any](session BuilderSession[BUILDER], f Handler[RES
//
// Example:
//
// err := postgres.ExecuteVoid(session, DeleteUser(123))
// err := octobe.ExecuteVoid(session, DeleteUser(123))
// if err != nil {
// return fmt.Errorf("failed to delete user: %w", err)
// }
Expand All @@ -229,7 +229,7 @@ func ExecuteVoid[BUILDER any](session BuilderSession[BUILDER], f Handler[Void, B
//
// Example:
//
// results, err := postgres.ExecuteMany(session,
// results, err := octobe.ExecuteMany(session,
// CreateUser("Alice"),
// CreateUser("Bob"),
// CreateUser("Charlie"),
Expand Down