Skip to content

uug-ai/database

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Database

Universal MongoDB database driver for Go with built-in observability and functional options pattern.

Go Version License GoDoc Go Report Card codecov Release

A Go library for connecting to and managing MongoDB with a unified interface using the functional options builder pattern. Includes built-in OpenTelemetry instrumentation for comprehensive observability.

Features

  • MongoDB Support: Full MongoDB driver integration with connection pooling
  • Options Builder Pattern: Clean, fluent interface for configuration
  • Built-in Validation: Compile-time type safety with validation
  • OpenTelemetry Integration: Distributed tracing and observability out of the box
  • Context Support: Full context.Context support for timeouts and cancellation
  • Comprehensive Tests: Full test coverage including mocks
  • Production Ready: Optimized for high-performance applications

Installation

go get github.com/uug-ai/database

Quick Start

package main

import (
    "context"
    "log"
    "time"
    "github.com/uug-ai/database/pkg/database"
)

func main() {
    // Build MongoDB options
    opts := database.NewMongoOptions().
        SetUri("mongodb://localhost:27017").
        SetHost("localhost").
        SetAuthSource("admin").
        SetAuthMechanism("SCRAM-SHA-256").
        SetReplicaSet("rs0").
        SetUsername("user").
        SetPassword("password").
        SetTimeout(10).
        Build()

    // Create database client with options
    db, err := database.New(opts)
    if err != nil {
        log.Fatal(err)
    }

    // Ping the database to verify connection
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := db.Ping(ctx); err != nil {
        log.Fatal(err)
    }

    log.Println("Successfully connected to MongoDB!")
}

Core Concepts

Options Builder Pattern

All components use the options builder pattern (similar to MongoDB's official driver). This provides:

  • Clean Syntax: Build options separately, then pass to constructor
  • Readability: Self-documenting method chains
  • Separation of Concerns: Options building is separate from client creation
  • Validation: Built-in validation when creating the client
  • Type Safety: Compile-time type checking
  • Flexibility: Configure only what you need

Creating a Database Client

Each database connection follows this pattern:

  1. Build Options using database.NewMongoOptions() with method chaining
  2. Call .Build() to get the options object
  3. Create Client by passing options to database.New(opts)
  4. Use the client for database operations

Usage Examples

MongoDB Connection

The MongoDB integration demonstrates the options builder pattern:

package main

import (
    "context"
    "log"
    "time"
    "github.com/uug-ai/database/pkg/database"
)

func main() {
    // Build MongoDB options
    opts := database.NewMongoOptions().
        SetUri("mongodb+srv://user:password@cluster.mongodb.net/?retryWrites=true&w=majority").
        SetHost("cluster.mongodb.net").
        SetAuthSource("admin").
        SetAuthMechanism("SCRAM-SHA-256").
        SetUsername("user").
        SetPassword("password").
        SetTimeout(30).
        SetRetryWrites(true).
        Build()

    // Create database client with options
    db, err := database.New(opts)
    if err != nil {
        log.Fatal(err)
    }

    // Perform database operations
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    err = db.Ping(ctx)
    if err != nil {
        log.Fatal(err)
    }

    log.Println("Connected to MongoDB successfully!")
}

Available Methods:

  • .SetUri(uri string) - MongoDB connection URI
  • .SetHost(host string) - Database host address
  • .SetAuthSource(source string) - Authentication source database
  • .SetAuthMechanism(mechanism string) - Authentication mechanism (e.g., SCRAM-SHA-256)
  • .SetReplicaSet(replicaSet string) - Replica set name
  • .SetUsername(username string) - Database username
  • .SetPassword(password string) - Database password
  • .SetTimeout(seconds int) - Connection timeout in seconds
  • .SetRetryWrites(retry bool) - Enable automatic retry writes
  • .Build() - Returns the MongoOptions object

Project Structure

.
├── pkg/
│   └── database/              # Core database implementation
│       ├── database.go        # Main Database struct
│       ├── mongodb.go         # MongoDB client implementation
│       ├── mongodb_test.go    # MongoDB tests
│       └── option.go          # Functional option types
├── main.go
├── go.mod
├── go.sum
├── Dockerfile
└── README.md

Configuration

Using the Options Builder Pattern (Recommended)

opts := database.NewMongoOptions().
    SetUri("mongodb://localhost:27017").
    SetHost("localhost").
    SetAuthSource("admin").
    SetAuthMechanism("SCRAM-SHA-256").
    SetReplicaSet("rs0").
    SetUsername("admin").
    SetPassword("password").
    SetTimeout(10).
    Build()

db, err := database.New(opts)

Environment Variables

You can load configuration from environment variables:

import "os"

opts := database.NewMongoOptions().
    SetUri(os.Getenv("MONGO_URI")).
    SetHost(os.Getenv("MONGO_HOST")).
    SetAuthSource(os.Getenv("MONGO_AUTH_SOURCE")).
    SetAuthMechanism(os.Getenv("MONGO_AUTH_MECHANISM")).
    SetReplicaSet(os.Getenv("MONGO_REPLICA_SET")).
    SetUsername(os.Getenv("MONGO_USERNAME")).
    SetPassword(os.Getenv("MONGO_PASSWORD")).
    SetTimeout(30).
    Build()

db, err := database.New(opts)

Example .env file:

# MongoDB Configuration
MONGO_URI=mongodb://localhost:27017
MONGO_HOST=localhost
MONGO_AUTH_SOURCE=admin
MONGO_AUTH_MECHANISM=SCRAM-SHA-256
MONGO_REPLICA_SET=rs0
MONGO_USERNAME=admin
MONGO_PASSWORD=password

Validation

MongoDB options use go-playground/validator for configuration validation. All required fields must be provided:

  • Uri - Connection URI (required)
  • Host - Database host (required)
  • AuthSource - Auth source database (required)
  • AuthMechanism - Auth mechanism type (required)
  • ReplicaSet - Replica set name (required)
  • Username - Database username (required)
  • Password - Database password (required)
  • Timeout - Connection timeout >= 0 (required)

Validation is automatically performed when calling database.New(opts), ensuring invalid configurations are caught before the client is created.

Error Handling

The options builder pattern provides clear error handling:

// Build options (no validation here)
opts := database.NewMongoOptions().
    SetUri("mongodb://localhost:27017").
    SetHost("localhost").
    // Missing required fields...
    Build()

// Validation happens when creating the client
db, err := database.New(opts)
if err != nil {
    // Validation error caught at client creation time
    log.Printf("Configuration error: %v", err)
    return
}

// If we get here, the configuration is valid
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err = db.Ping(ctx)
if err != nil {
    // Runtime error during operation
    log.Printf("Connection error: %v", err)
    return
}

Testing

Running Tests

Run the test suite:

go test ./...

Run tests with coverage:

go test -cover ./...

Run tests for specific components:

# Database tests
go test ./pkg/database -v

# MongoDB tests
go test ./pkg/database -run TestMongo

# Mock tests
go test ./pkg/database -run TestMockDatabase

Mocking for Tests

The package includes a complete mock implementation of the DatabaseInterface that allows you to control the behavior of database operations in your tests without needing a real database connection.

Basic Mock Usage

import (
    "context"
    "testing"
    "github.com/uug-ai/database/pkg/database"
)

func TestMyFunction(t *testing.T) {
    // Create a new mock database
    mock := database.NewMockDatabase()
    
    // Set up expectations for what the mock should return
    expectedUser := map[string]any{
        "id":   1,
        "name": "Alice",
        "email": "alice@example.com",
    }
    mock.ExpectFindOne(expectedUser, nil)
    
    // Inject the mock into your Database instance
    opts := database.NewMongoOptions().
        SetUri("mongodb://localhost").
        SetTimeout(5000).
        Build()
    
    db, err := database.New(opts, mock)
    if err != nil {
        t.Fatalf("failed to create database: %v", err)
    }
    
    // Use the database - it will use your mock
    result, err := db.Client.FindOne(context.Background(), "testdb", "users", map[string]any{"id": 1})
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    
    // Verify the call was tracked
    if len(mock.FindOneCalls) != 1 {
        t.Errorf("expected 1 FindOne call, got %d", len(mock.FindOneCalls))
    }
}

Advanced Mock Features

Expect Multiple Results:

mock := database.NewMockDatabase()

// Mock Find to return multiple documents
users := []map[string]any{
    {"id": 1, "name": "Alice"},
    {"id": 2, "name": "Bob"},
}
mock.ExpectFind(users, nil)

result, err := mock.Find(ctx, "testdb", "users", map[string]any{})
// result contains the mocked users

Expect Errors:

mock := database.NewMockDatabase()

// Mock a connection error
mock.ExpectPing(errors.New("connection failed"))

err := mock.Ping(ctx)
// err will be "connection failed"

Custom Behavior:

mock := database.NewMockDatabase()

// Define custom logic based on input
mock.FindFunc = func(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error) {
    filterMap := filter.(map[string]any)
    
    if filterMap["status"] == "active" {
        return []map[string]any{{"id": 1, "status": "active"}}, nil
    }
    
    return []map[string]any{}, nil
}

Sequential Responses (Queue Pattern):

mock := database.NewMockDatabase()

// Queue multiple responses for sequential calls
// Each call consumes the next item in the queue (FIFO)
users := []map[string]any{{"id": 1, "name": "Alice"}}
notifications := []map[string]any{{"id": 1, "message": "Hello"}}
settings := []map[string]any{{"key": "theme", "value": "dark"}}

mock.QueueFind(users, nil).
    QueueFind(notifications, nil).
    QueueFind(settings, nil)

// First call returns users
result1, _ := mock.Find(ctx, "testdb", "users", map[string]any{})
// result1 contains users

// Second call returns notifications
result2, _ := mock.Find(ctx, "testdb", "notifications", map[string]any{})
// result2 contains notifications

// Third call returns settings
result3, _ := mock.Find(ctx, "testdb", "settings", map[string]any{})
// result3 contains settings

// Fourth call falls back to default behavior (empty slice)
result4, _ := mock.Find(ctx, "testdb", "other", map[string]any{})
// result4 is []any{}

Queue with Errors:

mock := database.NewMockDatabase()

// Queue responses including errors
mock.QueueFind([]map[string]any{{"id": 1}}, nil).
    QueueFind(nil, errors.New("connection timeout")).
    QueueFind([]map[string]any{{"id": 2}}, nil)

// First call succeeds
result1, err := mock.Find(ctx, "testdb", "users", map[string]any{})
// err is nil, result1 has data

// Second call returns error
result2, err := mock.Find(ctx, "testdb", "users", map[string]any{})
// err is "connection timeout"

// Third call succeeds again
result3, err := mock.Find(ctx, "testdb", "users", map[string]any{})
// err is nil, result3 has data

Track Call History:

mock := database.NewMockDatabase()

// Make some calls
mock.Find(ctx, "testdb", "users", map[string]any{})
mock.FindOne(ctx, "testdb", "users", map[string]any{"id": 1})

// Verify the calls
if len(mock.FindCalls) != 1 {
    t.Error("expected 1 Find call")
}

if mock.FindCalls[0].Collection != "users" {
    t.Error("expected collection to be 'users'")
}

// Reset call history for the next test
mock.Reset()

Mock API

The MockDatabase type provides:

Setup Methods:

  • NewMockDatabase(): Creates a new mock with sensible defaults
  • ExpectPing(err error): Set expected Ping behavior (for all calls)
  • ExpectFind(result any, err error): Set expected Find behavior (for all calls)
  • ExpectFindOne(result any, err error): Set expected FindOne behavior (for all calls)

Sequential Queue Methods:

  • QueuePing(err error): Add a Ping response to the queue for sequential calls
  • QueueFind(result any, err error): Add a Find response to the queue for sequential calls
  • QueueFindOne(result any, err error): Add a FindOne response to the queue for sequential calls

Custom Function Handlers:

  • PingFunc: Custom function for Ping behavior
  • FindFunc: Custom function for Find behavior
  • FindOneFunc: Custom function for FindOne behavior

Call Tracking:

  • PingCalls: Slice of all Ping calls made
  • FindCalls: Slice of all Find calls made
  • FindOneCalls: Slice of all FindOne calls made

Utility Methods:

  • Reset(): Clear all call history and queues

Execution Priority:

  1. Queued responses (consumed FIFO) - highest priority
  2. Custom function handlers (Func properties)
  3. Default behavior - fallback

OpenTelemetry Integration

This package includes built-in OpenTelemetry instrumentation for MongoDB operations:

import (
    "github.com/uug-ai/database/pkg/database"
    "go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo"
)

// OpenTelemetry is automatically configured in NewMongoClient
opts := database.NewMongoOptions().
    SetUri("mongodb://localhost:27017").
    // ... other options
    Build()

db, err := database.New(opts)
// Automatic tracing enabled!

Contributing

Contributions are welcome! When adding new features or database drivers, please follow the options builder pattern demonstrated in this repository.

Development Guidelines

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/amazing-feature)
  3. Follow the options builder pattern
  4. Add comprehensive tests for your changes
  5. Ensure all tests pass: go test ./...
  6. Commit your changes following Conventional Commits
  7. Push to your branch (git push origin feat/amazing-feature)
  8. Open a Pull Request

Commit Message Format

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, types

Scopes:

  • database - Core database functionality
  • mongo - MongoDB driver
  • options - Options builder
  • docs - Documentation updates
  • tests - Test updates

Examples:

feat(mongo): add connection pooling configuration
fix(database): correct context timeout handling
docs(readme): update MongoDB connection examples
refactor(options): simplify builder interface
test(mongo): add replica set failover tests

License

This project is licensed under the MIT License - see the LICENSE file for details.

Dependencies

This project uses the following key libraries:

See go.mod for the complete list of dependencies.

Benefits of the Options Builder Pattern

Clean Syntax

Build options separately from client creation:

opts := database.NewMongoOptions().
    SetUri("mongodb://localhost:27017").
    SetHost("localhost").
    Build()

db, err := database.New(opts)

Separation of Concerns

Options building is completely separate from client creation, following the same pattern as MongoDB's official driver.

Type Safety

Compile-time type checking prevents configuration errors.

Flexibility

Configure only the options you need. Method chaining is optional.

Validation

Built-in validation when creating the client ensures configurations are correct before use, catching errors early.

Extensibility

Adding new builder methods doesn't break existing code. Simply add new chainable methods to the options builder.

Readability

Self-documenting fluent API makes code easy to read and understand:

// Clear and readable - MongoDB style
opts := database.NewMongoOptions().
    SetUri("mongodb+srv://user:password@cluster.mongodb.net").
    SetHost("cluster.mongodb.net").
    SetAuthSource("admin").
    SetAuthMechanism("SCRAM-SHA-256").
    SetUsername("user").
    SetPassword("password").
    SetTimeout(30).
    Build()

db, err := database.New(opts)

Support

About

A Go library implementing database clients

Resources

License

Stars

Watchers

Forks

Packages