Skip to content

shepherrrd/gontext

Repository files navigation

GoNtext - Entity Framework Core for Go

Go Version License GoDoc

GoNtext brings the familiar Entity Framework Core patterns to Go, providing a type-safe, LINQ-style ORM with automatic change tracking, migrations, and fluent querying capabilities.

✨ Features

  • 🎯 EF Core-Style API: Familiar patterns for .NET developers
  • 🔍 LINQ Queries: Type-safe querying with method chaining
  • 🏗️ Entity-based Queries: GORM-style struct patterns with comparison operators ✨
  • 📊 Enhanced Aggregations: Entity-based Sum, Average, Min, Max operations ✨
  • 🔗 Include/Select Support: Load relationships and specific fields
  • 📈 Change Tracking: Automatic entity change detection
  • 🔄 Migrations: Code-first database migrations with Go files
  • 💾 DbSets: Type-safe entity collections with generics
  • 🗃️ Multiple Databases: PostgreSQL, MySQL, SQLite support
  • 🐘 PostgreSQL Pascal Case: Automatic field name translation with quoted identifiers
  • 🚀 Zero Configuration: Automatic database-specific optimizations
  • ⚡ Field Validation: Runtime validation with clear error messages
  • 🔢 Comparison Operators: Support for >, <, >=, <=, != in all query patterns ✨

🚀 Quick Start

Installation

go get github.com/shepherrrd/gontext

Basic Setup

package main

import (
    "github.com/google/uuid"
    "github.com/shepherrrd/gontext"
)

// Define your entities
type User struct {
    ID       uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
    Username string    `gorm:"uniqueIndex;not null"`
    Email    string    `gorm:"uniqueIndex;not null"`
    Name     string    `gorm:"not null"`
}

// Create your DbContext
type AppContext struct {
    *gontext.DbContext
    Users *gontext.LinqDbSet[User]
}

func NewAppContext(connectionString string) (*AppContext, error) {
    ctx, err := gontext.NewDbContext(connectionString, "postgres")
    if err != nil {
        return nil, err
    }

    users := gontext.RegisterEntity[User](ctx)

    return &AppContext{
        DbContext: ctx,
        Users:     users,
    }, nil
}

🐘 PostgreSQL Pascal Case Support

GoNtext automatically handles PostgreSQL case-sensitive identifiers! No manual configuration required.

✨ How It Works

When using PostgreSQL, GoNtext automatically:

  • Uses struct names as table names: User struct → "User" table (not users)
  • Uses field names as column names: Username field → "Username" column
  • Quotes all identifiers: Proper PostgreSQL case-sensitive handling
  • Translates all queries: WHERE, ORDER BY, SELECT - everything works automatically

📝 Example

type User struct {
    ID           uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
    Username     string    `gorm:"uniqueIndex;not null"`
    Email        string    `gorm:"uniqueIndex;not null"`
    PasswordHash string    `gorm:"not null"`
    IsActive     bool      `gorm:"not null;default:true"`
    CreatedAt    time.Time `gorm:"autoCreateTime"`
}

// Setup - Zero configuration needed!
ctx, _ := gontext.NewDbContext("postgres://user:pass@localhost/db", "postgres")
userSet := gontext.RegisterEntity[User](ctx)

// All queries automatically use quoted PostgreSQL identifiers:
user, _ := userSet.WhereField("Username", "john").FirstOrDefault()
// SQL: SELECT * FROM "User" WHERE "Username" = 'john'

users, _ := userSet.OrderByField("Email").ToList()
// SQL: SELECT * FROM "User" ORDER BY "Email" ASC

userSet.WhereField("IsActive", true).Delete()
// SQL: DELETE FROM "User" WHERE "IsActive" = true

🎯 What You Get

  • Table Names: User struct becomes "User" table (Pascal case)
  • Column Names: Username field becomes "Username" column (Pascal case)
  • All Query Types: INSERT, SELECT, UPDATE, DELETE - all automatically translated
  • Complex Queries: WHERE with AND/OR/parentheses, LIKE, IN - all supported
  • Comparison Operators: Support for >, <, >=, <=, != in Where conditions ✨
  • Zero Boilerplate: No TableName() methods needed, no manual quoting

🚫 No More TableName() Methods

OLD WAY (not needed anymore):

func (User) TableName() string {
    return "User" // ❌ Don't do this anymore!
}

NEW WAY (automatic):

type User struct {
    // Just define your struct - GoNtext handles the rest! ✅
    Username string
    Email    string
}

GoNtext automatically uses the struct name (User) as the table name with proper PostgreSQL quoting.

🔢 Enhanced Query Patterns

GoNtext supports multiple query patterns for maximum flexibility!

🎯 Field-based Queries with Comparison Operators

// Age comparisons
users, _ := ctx.Users.Where("Age", ">18").ToList()         // Age > 18
users, _ := ctx.Users.Where("Age", ">=21").ToList()        // Age >= 21  
users, _ := ctx.Users.Where("Age", "<65").ToList()         // Age < 65
users, _ := ctx.Users.Where("Age", "<=30").ToList()        // Age <= 30
users, _ := ctx.Users.Where("Age", "!=25").ToList()        // Age != 25

// String comparisons
users, _ := ctx.Users.Where("Username", "!=admin").ToList() // Username != 'admin'

// Numeric fields
files, _ := ctx.Files.Where("Size", ">1048576").ToList()   // Size > 1MB

🏗️ Entity-based Queries (GORM-style)

// Static typing with entity structs
user, _ := ctx.Users.Where(&User{Email: "[email protected]"}).FirstOrDefault()

// Entity-based queries with comparison operators ✨ NEW!
users, _ := ctx.Users.Where(&User{Age: ">18"}).ToList()           // Age > 18
files, _ := ctx.Files.Where(&File{Size: ">=1048576"}).ToList()    // Size >= 1MB
users, _ := ctx.Users.Where(&User{Username: "!=admin"}).ToList()  // Username != 'admin'

// Combined entity patterns
activeAdults, _ := ctx.Users.Where(&User{IsActive: true, Age: ">=18"}).ToList()

🔗 Enhanced OR Operations

// Field-based OR with operators
users, _ := ctx.Users.Where("Age", ">=18").Or("Role", "admin").ToList()
users, _ := ctx.Users.Where("Status", "!=inactive").Or("Priority", ">5").ToList()

// Entity-based OR operations ✨ NEW!
users, _ := ctx.Users.Where(&User{Role: "admin"}).Or(&User{Age: ">=65"}).ToList()
users, _ := ctx.Users.Where(&User{Email: "[email protected]"}).Or(&User{Username: "admin"}).ToList()

// Mixed pattern OR operations
users, _ := ctx.Users.Where("IsActive", true).Or(&User{Role: "admin"}).ToList()

📊 Enhanced Aggregations & Ordering

GoNtext provides multiple patterns for aggregations and ordering!

🧮 Entity-based Aggregations ✨ NEW!

// Entity-based Sum - specify field using entity pattern
totalSize, _ := ctx.Files.Sum(&File{Size: 0})                    // Sum file sizes
totalRevenue, _ := ctx.Orders.Sum(&Order{Amount: 0.0})           // Sum order amounts

// Entity-based Average  
avgAge, _ := ctx.Users.Average(&User{Age: 0})                    // Average user age
avgRating, _ := ctx.Products.Average(&Product{Rating: 0.0})      // Average product rating

// Entity-based Min/Max
minPrice, _ := ctx.Products.Min(&Product{Price: 0.0})            // Minimum price
maxScore, _ := ctx.GameResults.Max(&GameResult{Score: 0})        // Maximum score

// Chain with WHERE conditions
totalAdultAge, _ := ctx.Users.Where("Age", ">=18").Sum(&User{Age: 0})
avgActiveUserAge, _ := ctx.Users.Where(&User{IsActive: true}).Average(&User{Age: 0})

📈 Traditional Field-based Aggregations

// Field name aggregations (backward compatible)
totalSize, _ := ctx.Files.SumField("Size")
avgAge, _ := ctx.Users.AverageField("Age")  
minPrice, _ := ctx.Products.MinField("Price")
maxScore, _ := ctx.GameResults.MaxField("Score")

// With comparison operators
expensiveItemsTotal, _ := ctx.Products.Where("Price", ">100").SumField("Price")
youngUsersAvgAge, _ := ctx.Users.Where("Age", "<25").AverageField("Age")

🔄 Enhanced Ordering Operations

// Multiple ordering patterns
users, _ := ctx.Users.OrderBy("Name").ToList()                     // String field name
users, _ := ctx.Users.OrderBy(func(u User) interface{} {            // Function selector
    return u.CreatedAt 
}).ToList()

// Descending order
users, _ := ctx.Users.OrderByDescending("Age").ToList()             // String field name  
users, _ := ctx.Users.OrderByDescending(func(u User) interface{} {  // Function selector
    return u.LastLogin 
}).ToList()

// Entity-based ordering (uses first non-zero field)
users, _ := ctx.Users.OrderByAscending(&User{Name: "placeholder"}).ToList()
users, _ := ctx.Users.OrderByDescendingEntity(&User{CreatedAt: time.Now()}).ToList()

// Chain with other operations
topSpenders, _ := ctx.Users.
    Where("IsActive", true).
    OrderByDescending("TotalSpent").
    Take(10).
    ToList()

🎯 Real-world Examples

// E-commerce analytics
type Product struct {
    ID          uint    `gorm:"primaryKey"`
    Name        string  `gorm:"not null"`
    Price       float64 `gorm:"not null"`
    Rating      float64 `gorm:"default:0"`
    InStock     bool    `gorm:"default:true"`
    CategoryID  uint    `gorm:"not null"`
}

// Get total inventory value for in-stock premium products
inventoryValue, _ := ctx.Products.
    Where(&Product{InStock: true}).
    Where("Price", ">50").
    Sum(&Product{Price: 0.0})

// Find average rating of highly-rated products
avgRating, _ := ctx.Products.
    Where("Rating", ">=4.0").
    Average(&Product{Rating: 0.0})

// Get top 5 most expensive products by category
topProducts, _ := ctx.Products.
    Where(&Product{CategoryID: 1}).
    OrderByDescending("Price").
    Take(5).
    ToList()

// Complex aggregation with OR conditions
totalValue, _ := ctx.Products.
    Where("Rating", ">=4.5").
    Or(&Product{InStock: true}).
    Sum(&Product{Price: 0.0})

🔗 Include & Select - Loading Relationships

GoNtext provides EF Core-style Include and Select functionality with type safety!

✨ Include Relationships

// Include single relationship
users, _ := ctx.Users.Include("Posts").ToList()

// Include multiple relationships
users, _ := ctx.Users.Include("Posts", "Profile").ToList()

// Auto-include all relationships
users, _ := ctx.Users.IncludeAll().ToList()

// Chain with other operations
users, _ := ctx.Users.Include("Posts").Where("IsActive", true).ToList()

🎯 Select Specific Fields

// Select only specific fields
users, _ := ctx.Users.Select("ID", "Username", "Email").ToList()

// Exclude sensitive fields
users, _ := ctx.Users.Omit("PasswordHash").ToList()

// Combine Include and Select
users, _ := ctx.Users.Include("Posts").Select("ID", "Username").ToList()

⚡ Type Safety & Validation

// ✅ Valid - compiles and runs
users, _ := ctx.Users.Include("Posts").ToList()

// ❌ Invalid - panics with: Field 'Post' not found on User
users, _ := ctx.Users.Include("Post").ToList()  // Typo in field name

GoNtext validates field names at runtime and provides clear error messages for fast debugging.

🚀 Real-World Example

// Load users with their posts and profiles, excluding sensitive data
results, _ := ctx.Users.
    Include("Posts", "Profile").
    Omit("PasswordHash").
    Where("IsActive", true).
    OrderBy("Username").
    Skip(0).Take(10).
    ToList()

// Auto-detect and load all relationships
users, _ := ctx.Users.IncludeAll().ToList()

🏷️ Custom Table Names

You can override the default table naming by implementing the TableName() method:

📝 Example

type User struct {
    Id       uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
    Username string    `gorm:"uniqueIndex;not null"`
    Email    string    `gorm:"uniqueIndex;not null"`
}

// Custom table name - overrides default "User"
func (User) TableName() string {
    return "app_users" // Will create "app_users" table instead of "User"
}

type Product struct {
    Id   uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()"`
    Name string    `gorm:"not null"`
}

// No TableName() method - uses default "Product" table name

✨ How It Works

GoNtext respects the TableName() method across all operations:

  • CRUD Operations: ctx.Users.Add(), ctx.Users.Find(), etc. use custom table name
  • LINQ Queries: ctx.Users.WhereField().ToList() uses custom table name
  • Migrations: Migration files generate SQL for custom table names
  • PostgreSQL: Automatically quotes custom table names: "app_users"
// Setup with custom table names
ctx, _ := gontext.NewDbContext("postgres://user:pass@localhost/db", "postgres")
userSet := gontext.RegisterEntity[User](ctx)   // Uses "app_users" table
productSet := gontext.RegisterEntity[Product](ctx) // Uses "Product" table

// All operations use the custom table names automatically
user, _ := userSet.WhereField("Username", "john").FirstOrDefault()
// SQL: SELECT * FROM "app_users" WHERE "Username" = 'john'

product, _ := productSet.WhereField("Name", "laptop").FirstOrDefault()
// SQL: SELECT * FROM "Product" WHERE "Name" = 'laptop'

🎯 When to Use Custom Table Names

  • Legacy databases: Match existing table names
  • Naming conventions: Follow company/team standards (e.g., tbl_users, app_users)
  • Multi-tenant: Different table prefixes per tenant
  • Database conventions: Snake_case, plural names, etc.

🎯 GORM-Style Static Typing

GoNtext now supports GORM-style static typing with struct patterns! Use familiar GORM syntax alongside EF Core-style LINQ methods.

✨ Features

  • 🔍 Struct-based Where Clauses: Use &User{Email: "[email protected]"} instead of strings
  • 🚀 Multiple Query Patterns: Support for SQL, field names, and struct patterns
  • 🔗 OR Operations: Chain WHERE and OR conditions with struct patterns
  • ⚡ GORM-Compatible: Drop-in replacement for common GORM operations
  • 🎯 Type-Safe: Compile-time checking with struct patterns

📝 Query Patterns

GoNtext supports 3 query patterns:

// Pattern 1: SQL with parameters (traditional)
user, _ := ctx.Users.Where("Email = ?", "[email protected]").FirstOrDefault()

// Pattern 2: Field name with value (EF Core style)
user, _ := ctx.Users.Where("Email", "[email protected]").FirstOrDefault()

// Pattern 3: Struct pattern (GORM style) ✨ NEW!
user, _ := ctx.Users.Where(&entities.User{Email: "[email protected]"}).FirstOrDefault()

🔗 OR Operations

Use OR conditions with struct patterns for complex queries:

// Login with email OR username (like GORM)
user, _ := ctx.Users.Where(&entities.User{Email: "[email protected]"}).
                    OrField("Username", "john").
                    FirstOrDefault()

// Multiple OR conditions with structs
users, _ := ctx.Users.Where(&entities.User{Role: "admin"}).
                      OrEntity(entities.User{Role: "manager"}).
                      ToList()

// Mixed patterns
user, _ := ctx.Users.Where("IsActive", true).
                     OrField("Role", "admin").
                     FirstOrDefault()

🚀 GORM-Style CRUD Operations

Use familiar GORM patterns with GoNtext's change tracking:

// Create (GORM style)
user := &entities.User{Username: "john", Email: "[email protected]"}
err := ctx.Users.Create(user)

// Save (creates or updates like GORM)
user.Email = "[email protected]"
err := ctx.Users.Save(user)

// First with struct pattern (like GORM)
user, err := ctx.Users.First(&entities.User{Email: "[email protected]"})

// Update with struct pattern
user.Username = "newusername"
err := ctx.Users.Update(*user)

// Find by ID (GORM style)
user, err := ctx.Users.Find(userID)

🎯 Login Example

Perfect for authentication with email OR username:

func LoginUser(emailOrUsername, password string) (*User, error) {
    // Search by email OR username using static typing
    user, err := ctx.Users.WhereField("email", emailOrUsername).
                           OrField("username", emailOrUsername).
                           FirstOrDefault()

    if err != nil || user == nil {
        return nil, fmt.Errorf("invalid credentials")
    }

    // Verify password...
    return user, nil
}

⚡ Performance & Compatibility

  • Zero Runtime Overhead: Struct patterns compile to optimized SQL
  • PostgreSQL Optimized: Automatic field name translation with quoted identifiers
  • GORM Compatible: Familiar methods work the same way
  • Change Tracking: Automatic entity state management like EF Core

📚 Examples

Choose what you want to learn:

Learn the fundamentals:

  • Creating entities
  • Saving changes
  • Querying data
  • Updates and deletes

Database schema management:

  • Creating migrations
  • Model snapshots
  • Schema evolution
  • Database updates

Advanced querying:

  • Where conditions
  • Ordering and pagination
  • String operations
  • Aggregations
  • Method chaining

⚠️ Important: Migration Setup

The built-in CLI has limitations. For proper migrations, you need to set up entity registration:

Step 1: Create Design-Time Context

// File: migrations_context.go
func CreateDesignTimeContext() (*gontext.DbContext, error) {
    ctx, err := gontext.NewDbContext("your-db-url", "postgres")
    if err != nil {
        return nil, err
    }

    // Register ALL your entities
    gontext.RegisterEntity[User](ctx)
    gontext.RegisterEntity[Post](ctx)
    // ... register every entity

    return ctx, nil
}

Step 2: Add Migration Commands

// Add to your main.go
func handleMigrations() {
    if len(os.Args) > 1 && os.Args[1] == "migrate:add" {
        // Use your design-time context for migrations
        ctx, _ := CreateDesignTimeContext()
        // Generate migration files
    }
}

See Migrations Example for complete setup.

🎯 Why GoNtext?

  • 🎯 Familiar: Uses EF Core patterns you already know
  • 🔍 Type-Safe: LINQ queries with compile-time checking
  • 🚀 Powerful: Full SQL capabilities via GORM integration
  • 📦 Complete: Migrations, change tracking, and DbContext patterns
  • 🔄 Flexible: Use LINQ or drop down to raw SQL when needed

GoNtext brings the best of Entity Framework Core to Go!

🤝 Framework Comparison

EF Core vs GoNtext

EF Core (C#) GoNtext (Go)
context.Users.Add(user) ctx.Users.Add(user)
context.SaveChanges() ctx.SaveChanges()
context.Users.Where(x => x.IsActive).ToList() ctx.Users.Where("IsActive", true).ToList()
context.Users.Where(x => x.IsActive).ToList() ctx.Users.Where(&User{IsActive: true}).ToList()
context.Users.FirstOrDefault(x => x.Id == id) ctx.Users.ById(id)
context.Users.FirstOrDefault(x => x.Id == id) ctx.Users.Where(&User{Id: id}).FirstOrDefault()
context.Users.OrderBy(x => x.Email) ctx.Users.OrderBy("Email")
context.Users.Where(x => x.Age > 18) ctx.Users.Where("Age", ">18")
context.Users.Where(x => x.Age > 18) ctx.Users.Where(&User{Age: ">18"})NEW!
context.Orders.Sum(x => x.Amount) ctx.Orders.Sum(&Order{Amount: 0.0})NEW!
context.Users.Average(x => x.Age) ctx.Users.Average(&User{Age: 0})NEW!
Pascal case tables (Users) Pascal case tables ("User") ✨
Pascal case columns (IsActive) Pascal case columns ("IsActive") ✨
[Table("app_users")] class User func (User) TableName() string { return "app_users" }

Traditional GORM vs GoNtext

Traditional GORM (Go) GoNtext (Go)
db.Where(&User{Email: "test"}).First(&user) ctx.Users.Where(&User{Email: "test"}).FirstOrDefault()
db.Where("email = ?", email).Or("username = ?", username).First(&user) ctx.Users.Where("Email", email).Or("Username", username).FirstOrDefault()
db.Where("age > ?", 18).Find(&users) ctx.Users.Where("Age", ">18").ToList()
db.Where("age > ?", 18).Find(&users) ctx.Users.Where(&User{Age: ">18"}).ToList()NEW!
db.Create(&user) ctx.Users.Add(user) ✨ (EF Core style)
db.Save(&user) ctx.Users.Save(&user)
db.First(&user, id) ctx.Users.Find(id)
db.Model(&Order{}).Select("SUM(amount)").Scan(&total) ctx.Orders.Sum(&Order{Amount: 0.0})NEW!
db.Model(&User{}).Select("AVG(age)").Scan(&avg) ctx.Users.Average(&User{Age: 0})NEW!
Manual change tracking Automatic change tracking ✨
Manual migrations Code-first migrations ✨
Snake_case by default Pascal case with PostgreSQL ✨

🎯 Best of Both Worlds

GoNtext combines the best features:

  • 🎯 EF Core: Change tracking, DbContext patterns, LINQ-style queries
  • ⚡ GORM: Familiar syntax, struct-based queries, flexible operations
  • 🐘 PostgreSQL: Automatic Pascal case with quoted identifiers
  • 🚀 Performance: Zero runtime overhead, optimized SQL generation

📖 Documentation

🏃‍♂️ Quick Test

# Clone and test
git clone https://github.com/shepherrrd/gontext
cd gontext/examples/01-crud
go mod tidy
createdb test_gontext
go run .

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages