Testing & Environments

Enterprise applications need to seamlessly switch databases based on the environment: a robust PostgreSQL instance for Production, and a fast, ephemeral SQLite instance for local testing.

By combining Rays ORM with the core framework’s ConditionalOnProfile and ConfigurationProperties, you can create a highly scalable environment strategy.

1. The Configuration Files

Define your connection strings in your standard YAML configuration files.

# config/application.yaml (Default / Testing)
app:
  database:
    dsn: "file::memory:"
# config/application-prod.yaml (Loaded only when 'prod' profile is active)
app:
  database:
    dsn: "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable"

2. The Dialector Strategy

Create an interface to abstract the GORM Dialector opening logic, and implement it for both SQLite and Postgres.

package config

import (
    "gorm.io/driver/postgres"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    . "github.com/BeamFoundry/rays/pkg/core"
)

// 1. The Abstraction
type DBOpener interface {
    Open(string) gorm.Dialector
}

// 2. The SQLite Implementation
type SQLiteDB struct{}

func (this *SQLiteDB) Open(dsn string) gorm.Dialector {
    return sqlite.Open(dsn)
}

// 3. The Postgres Implementation
type PostgresDB struct{}

func (this *PostgresDB) Open(dsn string) gorm.Dialector {
    return postgres.Open(dsn)
}

// 4. The Conditional Dialector Configuration
type DialectorConfig struct {
    Configuration
}

// Loads Postgres ONLY if the 'prod' profile is active
func (this *DialectorConfig) DBOpenerProd(
    _ struct{ ConditionalOnProfile `Name:"prod"` },
) DBOpener {
    return &PostgresDB{}
}

// Fallback to SQLite if no other DBOpener has been registered
func (this *DialectorConfig) DBOpenerFallback(
    _ struct{ ConditionalOnMissingBeam },
) DBOpener {
    return &SQLiteDB{}
}

var _, _ = any(&DialectorConfig{}).(Stereotype)

3. The Unified Database Provider

Finally, create your actual database config. Inject the resolved properties and the conditionally loaded DBOpener.

package config

import (
    "gorm.io/gorm"
    . "github.com/BeamFoundry/rays/pkg/core"
    . "github.com/BeamFoundry/rays/pkg/lang"
)

// Bind the YAML properties
type DBConfigProperties struct {
    ConfigurationProperties `Name:"app.database"`
    dsn string
}
var _, _ = any(&DBConfigProperties{}).(Stereotype)

// The Database Provider
type DbConfig struct {
    Configuration
    properties *DBConfigProperties `@:"Inject"`
    dbOpener   DBOpener            `@:"Inject"`
}

func (this *DbConfig) DB() (*gorm.DB, Error) {
    // Open the DB using the conditionally injected dialector and properties!
    if db, err := gorm.Open(this.dbOpener.Open(this.properties.dsn), &gorm.Config{}); err == nil {
        return db, nil
    } else {
        return nil, NewErrorFrom(err)
    }
}

var _, _ = any(&DbConfig{}).(Stereotype)

Zero-Config Testing

With this architecture, testing becomes incredibly frictionless. When you run your Ginkgo test suites using the Rays TestingContainer, the prod profile is not active by default.

The container will automatically fall back to the SQLiteDB opener and the file::memory: DSN from your base application.yaml, giving you a pristine, isolated database for every test run without changing a single line of test code!