A lightweight and intuitive ORM for Go, inspired by (the original Java version)[https://github.com/wilburhimself/theory_java].
- Simple and intuitive API for database operations
- Support for SQLite (more databases planned)
- Type-safe query building
- Flexible model metadata definition
- Customizable table and field names
- Robust error handling
- Migration support
- Connection pooling (planned)
go get github.com/wilburhimself/theory
package main
import (
"github.com/wilburhimself/theory"
_ "github.com/mattn/go-sqlite3" // SQLite driver
)
func main() {
// Initialize the ORM
db, err := theory.Connect(theory.Config{
Driver: "sqlite3",
DSN: "file:test.db?cache=shared&mode=memory",
})
if err != nil {
panic(err)
}
defer db.Close()
}
There are multiple ways to define your models in Theory:
The simplest way is to use struct tags to define your model's metadata:
type User struct {
ID int `db:"id,pk,auto"` // Primary key with auto-increment
Name string `db:"name"` // Regular field
Email string `db:"email,null"` // Nullable field
}
Available struct tag options:
pk
: Marks the field as a primary keyauto
: Enables auto-increment for numeric primary keysnull
: Allows the field to be NULL in the databasedb:"-"
: Excludes the field from database operations
For more control over your model's metadata, you can implement the Model interface:
type User struct {
ID int
Name string
Email string
}
func (u *User) TableName() string {
return "custom_users" // Custom table name
}
func (u *User) PrimaryKey() *model.Field {
return &model.Field{
Name: "ID",
DBName: "id",
Type: reflect.TypeOf(0),
IsPK: true,
IsAuto: true,
}
}
For complete control over your model's metadata:
func (u *User) ExtractMetadata() (*model.Metadata, error) {
return &model.Metadata{
TableName: "custom_users",
Fields: []model.Field{
{Name: "ID", DBName: "id", Type: reflect.TypeOf(0), IsPK: true, IsAuto: true},
{Name: "Name", DBName: "name", Type: reflect.TypeOf("")},
{Name: "Email", DBName: "email", Type: reflect.TypeOf(""), IsNull: true},
},
}, nil
}
All CRUD operations require a context:
user := &User{
Name: "John Doe",
Email: "[email protected]",
}
err := db.Create(context.Background(), user)
if err != nil {
panic(err)
}
Find a single record:
user := &User{}
err := db.First(context.Background(), user, 1) // Find by primary key
if err == theory.ErrRecordNotFound {
// Handle not found case
}
Find multiple records:
var users []User
err := db.Find(context.Background(), &users, "age > ?", 18)
user.Name = "Jane Doe"
err := db.Update(context.Background(), user)
err := db.Delete(context.Background(), user)
Theory provides a robust migration system that supports both automatic migrations based on models and manual migrations for more complex schema changes.
The simplest way to manage your database schema is using auto-migrations:
type User struct {
ID int `db:"id,pk,auto"`
Name string `db:"name"`
Email string `db:"email,null"`
CreatedAt time.Time `db:"created_at"`
}
// Create or update tables based on models
err := db.AutoMigrate(&User{})
if err != nil {
panic(err)
}
For more complex schema changes, you can create manual migrations:
func createUserMigration() *migration.Migration {
m := migration.NewMigration("create_users_table")
// Define up operations
m.Up = []migration.Operation{
&migration.CreateTable{
Name: "users",
Columns: []migration.Column{
{Name: "id", Type: "INTEGER", IsPK: true, IsAuto: true},
{Name: "name", Type: "TEXT", IsNull: false},
{Name: "email", Type: "TEXT", IsNull: true},
},
ForeignKeys: []migration.ForeignKey{
{
Columns: []string{"team_id"},
RefTable: "teams",
RefColumns: []string{"id"},
OnDelete: "CASCADE",
OnUpdate: "CASCADE",
},
},
Indexes: []migration.Index{
{
Name: "idx_users_email",
Columns: []string{"email"},
Unique: true,
},
},
},
}
// Define down operations
m.Down = []migration.Operation{
&migration.DropTable{Name: "users"},
}
return m
}
Theory provides several ways to run migrations:
// Create a new migrator
migrator := migration.NewMigrator(db)
// Add migrations
migrator.Add(createUserMigration())
migrator.Add(createTeamMigration())
// Run all pending migrations in a transaction
err := migrator.Up()
if err != nil {
panic(err)
}
// Roll back the last batch of migrations
err = migrator.Down()
if err != nil {
panic(err)
}
// Check migration status
status, err := migrator.Status()
if err != nil {
panic(err)
}
for _, s := range status {
fmt.Printf("Migration: %s, Applied: %v, Batch: %d\n",
s.Migration.Name,
s.Applied != nil,
s.Batch)
}
Theory's migration system supports:
- Foreign Keys: Define relationships between tables with ON DELETE and ON UPDATE actions
- Indexes: Create and drop indexes, including unique constraints
- Batch Migrations: Run multiple migrations as a single transaction
- Rollback Support: Easily roll back migrations by batch
- Migration Status: Track which migrations have been applied and when
- Error Handling: Robust error handling with descriptive messages
- Validation: Type validation for SQLite column types
Available migration operations:
CreateTable
: Create a new table with columns, foreign keys, and indexesDropTable
: Remove an existing tableAddColumn
: Add a new column to an existing tableModifyColumn
: Modify an existing column's propertiesCreateIndex
: Create a new index on specified columnsDropIndex
: Remove an existing indexAddForeignKey
: Add a new foreign key constraint
Theory provides clear error types for common scenarios:
// Record not found
if err == theory.ErrRecordNotFound {
// Handle not found case
}
// Other errors
if err != nil {
// Handle other errors
}
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.