zorm

package module
v0.6.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 30, 2025 License: MIT Imports: 16 Imported by: 0

README

Go Reference Go Report Card codecov

ZORM

A Type-Safe, Production Ready Go ORM

One ORM To Query Them All


ZORM is a powerful, type-safe, and developer-friendly Go ORM designed for modern applications. It leverages Go generics to provide compile-time type safety while offering a fluent, chainable API for building complex SQL queries with ease.

Key Features

  • Type-Safe: Full compile-time type safety powered by Go generics
  • Zero Dependencies: Built on Go's database/sql package, works with any SQL driver
  • High Performance: Prepared statement caching and connection pooling
  • Relations: HasOne, HasMany, BelongsTo, BelongsToMany, Polymorphic relations
  • Fluent API: Chainable query builder with intuitive method names
  • Advanced Queries: CTEs, Subqueries, Full-Text Search, Window Functions
  • Database Splitting: Automatic read/write split with replica support
  • Context Support: All operations respect context.Context for cancellation & timeout
  • Debugging: Print() method to inspect generated SQL without executing
  • Lifecycle Hooks: BeforeCreate, BeforeUpdate, AfterUpdate hooks
  • Accessors: Computed attributes via getter methods

Installation

go get github.com/rezakhademix/zorm

Quick Start

1. Connect to Database

PostgreSQL
import (
    "github.com/rezakhademix/zorm"
)

// Using helper (with connection pooling)
db, err := zorm.ConnectPostgres(
    "postgres://user:password@localhost/dbname?sslmode=disable",
    &zorm.DBConfig{
        MaxOpenConns:    25,
        MaxIdleConns:    5,
        ConnMaxLifetime: time.Hour,
        ConnMaxIdleTime: 30 * time.Minute,
    },
)

zorm.GlobalDB = db

2. Define Models

Models are standard Go structs. ZORM uses convention over configuration - no tags required!

type User struct {
    ID        int64      // Automatically detected as primary key with auto-increment
    Name      string     // Maps to "name" column
    Email     string     // Maps to "email" column
    Age       int        // Maps to "age" column
    CreatedAt time.Time  // Maps to "created_at" column
    UpdatedAt time.Time  // Maps to "updated_at" (auto-updated)
}
// Table name: "users" (auto-pluralized snake_case)
Custom Table Name & Primary Key
// Custom table name
func (u User) TableName() string {
    return "app_users"
}

// Custom primary key
func (u User) PrimaryKey() string {
    return "user_id"
}

3. Basic CRUD

ctx := context.Background()

// Create
user := &User{Name: "John", Email: "[email protected]"}
err := zorm.New[User]().Create(ctx, user)
fmt.Println(user.ID) // Auto-populated after insert

// Read - Single
user, err := zorm.New[User]().Find(ctx, 1)
user, err := zorm.New[User]().Where("email", "[email protected]").First(ctx)

// Read - Multiple
users, err := zorm.New[User]().Where("age", ">", 18).Get(ctx)

// Update
user.Name = "Jane"
err = zorm.New[User]().Update(ctx, user) // updated_at auto-set

// Delete
err = zorm.New[User]().Where("id", 1).Delete(ctx)

API Reference

Query Methods

Method Description Returns
Get(ctx) Execute query and return all results []*T, error
First(ctx) Execute query and return first result *T, error
Find(ctx, id) Find record by primary key *T, error
FindOrFail(ctx, id) Find record or return error *T, error
Exists(ctx) Check if any record matches bool, error
Count(ctx) Count matching records int64, error
Sum(ctx, column) Sum of column values float64, error
Avg(ctx, column) Average of column values float64, error
Pluck(ctx, column) Get single column values []any, error

Write Methods

Method Description
Create(ctx, entity) Insert single record
CreateMany(ctx, entities) Insert multiple records
Update(ctx, entity) Update single record by primary key
UpdateMany(ctx, values) Update multiple records matching query
Delete(ctx) Delete records matching query
DeleteMany(ctx) Alias for Delete
FirstOrCreate(ctx, attrs, values) Find first or create new
UpdateOrCreate(ctx, attrs, values) Update existing or create new

Query Builder Methods

Method Description
Select(columns...) Specify columns to select
Distinct() Add DISTINCT to query
DistinctBy(columns...) PostgreSQL DISTINCT ON
Where(query, args...) Add WHERE condition
OrWhere(query, args...) Add OR WHERE condition
WhereIn(column, values) WHERE column IN (...)
WhereNull(column) WHERE column IS NULL
WhereNotNull(column) WHERE column IS NOT NULL
OrWhereNull(column) OR column IS NULL
OrWhereNotNull(column) OR column IS NOT NULL
WhereHas(relation, callback) WHERE EXISTS subquery
OrderBy(column, direction) Add ORDER BY
Latest(column?) ORDER BY column DESC
Oldest(column?) ORDER BY column ASC
GroupBy(columns...) Add GROUP BY
Having(query, args...) Add HAVING
Limit(n) Set LIMIT
Offset(n) Set OFFSET
Lock(mode) Add FOR UPDATE/SHARE

Utility Methods

Method Description
Clone() Deep copy the query builder
Table(name) Override table name
TableName() Get current table name
SetDB(db) Set custom DB connection
WithTx(tx) Use transaction
WithContext(ctx) Set context
WithStmtCache(cache) Enable statement caching
Scope(fn) Apply reusable query logic
Print() Get SQL without executing
Raw(sql, args...) Set raw SQL query
Exec(ctx) Execute raw query

Query Builder Details

Where Conditions

// Equality
zorm.New[User]().Where("name", "John").Get(ctx)

// Operators
zorm.New[User]().Where("age", ">", 18).Get(ctx)
zorm.New[User]().Where("email", "LIKE", "%@example.com").Get(ctx)
zorm.New[User]().Where("status", "!=", "inactive").Get(ctx)

// Map (multiple AND conditions)
zorm.New[User]().Where(map[string]any{
    "name": "John",
    "age":  25,
}).Get(ctx)

// Struct (non-zero fields)
zorm.New[User]().Where(&User{Name: "John", Age: 25}).Get(ctx)

// Nested/Grouped conditions
zorm.New[User]().Where(func(q *zorm.Model[User]) {
    q.Where("role", "admin").OrWhere("role", "manager")
}).Where("active", true).Get(ctx)
// WHERE (role = 'admin' OR role = 'manager') AND active = true

// NULL checks
zorm.New[User]().WhereNull("deleted_at").Get(ctx)
zorm.New[User]().WhereNotNull("verified_at").Get(ctx)

// IN clause
zorm.New[User]().WhereIn("id", []any{1, 2, 3}).Get(ctx)

// OR conditions
zorm.New[User]().Where("age", ">", 18).OrWhere("verified", true).Get(ctx)

Exists Check

// Check if any matching record exists (efficient - uses SELECT 1 LIMIT 1)
exists, err := zorm.New[User]().Where("email", "[email protected]").Exists(ctx)
if exists {
    fmt.Println("User exists!")
}

Pluck (Single Column)

// Get just the email column from all users
emails, err := zorm.New[User]().Where("active", true).Pluck(ctx, "email")
for _, email := range emails {
    fmt.Println(email)
}

Cursor (Memory-Efficient Iteration)

For large datasets, use Cursor to iterate row by row without loading everything into memory:

cursor, err := zorm.New[User]().Where("active", true).Cursor(ctx)
if err != nil {
    return err
}
defer cursor.Close()

for cursor.Next() {
    user, err := cursor.Scan(ctx)
    if err != nil {
        return err
    }
    // Process user one at a time
    fmt.Println(user.Name)
}

FirstOrCreate & UpdateOrCreate

// Find first matching record, or create if not found
user, err := zorm.New[User]().FirstOrCreate(ctx,
    map[string]any{"email": "[email protected]"},  // Search attributes
    map[string]any{"name": "John", "age": 25},    // Values for creation
)

// Find and update, or create if not found
user, err := zorm.New[User]().UpdateOrCreate(ctx,
    map[string]any{"email": "[email protected]"},  // Search attributes
    map[string]any{"name": "John Updated"},       // Values to set
)

Pagination

// Full pagination (with total count - 2 queries)
result, err := zorm.New[User]().Paginate(ctx, 1, 15)
fmt.Println(result.Data)        // []*User
fmt.Println(result.Total)       // Total record count
fmt.Println(result.CurrentPage) // 1
fmt.Println(result.LastPage)    // Calculated last page
fmt.Println(result.PerPage)     // 15

// Simple pagination (no count - 1 query, faster)
result, err := zorm.New[User]().SimplePaginate(ctx, 1, 15)
// result.Total will be -1 (skipped)

Clone (Reuse Queries Safely)

baseQuery := zorm.New[User]().Where("active", true)

// Clone prevents modifying original
admins, _ := baseQuery.Clone().Where("role", "admin").Get(ctx)
users, _ := baseQuery.Clone().Limit(10).Get(ctx)

// Original is unchanged
all, _ := baseQuery.Get(ctx)

Custom Table Name

// Override table name for this query
users, _ := zorm.New[User]().Table("archived_users").Get(ctx)

Lifecycle Hooks

ZORM supports lifecycle hooks that are automatically called during CRUD operations.

Available Hooks

Hook When Called
BeforeCreate(ctx) Before INSERT
BeforeUpdate(ctx) Before UPDATE
AfterUpdate(ctx) After UPDATE

Implementing Hooks

type User struct {
    ID        int64
    Name      string
    Email     string
    CreatedAt time.Time
    UpdatedAt time.Time
}

// BeforeCreate is called before inserting a new record
func (u *User) BeforeCreate(ctx context.Context) error {
    // Validate
    if u.Email == "" {
        return errors.New("email is required")
    }

    // Set defaults
    u.CreatedAt = time.Now()

    // Normalize data
    u.Email = strings.ToLower(u.Email)

    return nil
}

// BeforeUpdate is called before updating a record
func (u *User) BeforeUpdate(ctx context.Context) error {
    // Validate
    if u.Name == "" {
        return errors.New("name cannot be empty")
    }

    // updated_at is set automatically by ZORM

    return nil
}

// AfterUpdate is called after a successful update
func (u *User) AfterUpdate(ctx context.Context) error {
    // Log, send notifications, update cache, etc.
    log.Printf("User %d updated", u.ID)
    return nil
}

Hook Execution Flow

// Create flow:
// 1. BeforeCreate(ctx) called
// 2. INSERT executed
// 3. ID populated

user := &User{Name: "John", Email: "[email protected]"}
err := zorm.New[User]().Create(ctx, user)
// BeforeCreate lowercases email to "[email protected]"

// Update flow:
// 1. updated_at set automatically
// 2. BeforeUpdate(ctx) called
// 3. UPDATE executed
// 4. AfterUpdate(ctx) called

user.Name = "Jane"
err = zorm.New[User]().Update(ctx, user)

Accessors (Computed Attributes)

Define getter methods to compute virtual attributes. Methods starting with Get are automatically called after scanning. The struct must have an Attributes map[string]any field to store computed values.

type User struct {
    ID         int64
    FirstName  string
    LastName   string
    Attributes map[string]any // Holds computed values
}

// Accessor: GetFullName -> attributes["full_name"]
func (u *User) GetFullName() string {
    return u.FirstName + " " + u.LastName
}

// Accessor: GetInitials -> attributes["initials"]
func (u *User) GetInitials() string {
    return string(u.FirstName[0]) + string(u.LastName[0])
}

// Usage
user, _ := zorm.New[User]().Find(ctx, 1)
fmt.Println(user.Attributes["full_name"])  // "John Doe"
fmt.Println(user.Attributes["initials"])   // "JD"

Relationships

Defining Relations

Relations are defined as methods on your model that return a relation type. The method name can be either RelationName or RelationNameRelation (e.g., Posts or PostsRelation).

type User struct {
    ID      int64
    Name    string
    Posts   []*Post  // HasMany
    Profile *Profile // HasOne
}

// HasMany: User has many Posts
// Method can be named "Posts" or "PostsRelation"
func (u User) PostsRelation() zorm.HasMany[Post] {
    return zorm.HasMany[Post]{
        ForeignKey: "user_id",  // Column in posts table
        LocalKey:   "id",       // Optional, defaults to primary key
    }
}

// HasOne: User has one Profile
func (u User) ProfileRelation() zorm.HasOne[Profile] {
    return zorm.HasOne[Profile]{
        ForeignKey: "user_id",
    }
}

type Post struct {
    ID     int64
    UserID int64
    Title  string
    Author *User    // BelongsTo
}

// BelongsTo: Post belongs to User
func (p Post) AuthorRelation() zorm.BelongsTo[User] {
    return zorm.BelongsTo[User]{
        ForeignKey: "user_id",  // Column in posts table
        OwnerKey:   "id",       // Optional, defaults to primary key
    }
}

Custom Table Names in Relations

func (u User) PostsRelation() zorm.HasMany[Post] {
    return zorm.HasMany[Post]{
        ForeignKey: "user_id",
        Table:      "blog_posts",  // Use custom table name
    }
}

Eager Loading

// Load single relation (use the relation name without "Relation" suffix)
users, _ := zorm.New[User]().With("Posts").Get(ctx)

// Load multiple relations
users, _ := zorm.New[User]().With("Posts", "Profile").Get(ctx)

// Load nested relations
users, _ := zorm.New[User]().With("Posts.Comments").Get(ctx)

// Load with constraints
users, _ := zorm.New[User]().WithCallback("Posts", func(q *zorm.Model[Post]) {
    q.Where("published", true).
      OrderBy("created_at", "DESC").
      Limit(5)
}).Get(ctx)

Lazy Loading

user, _ := zorm.New[User]().Find(ctx, 1)

// Load relation on existing entity
err := zorm.New[User]().Load(ctx, user, "Posts")

// Load on slice
users, _ := zorm.New[User]().Get(ctx)
err := zorm.New[User]().LoadSlice(ctx, users, "Posts", "Profile")

Many-to-Many Relations

type User struct {
    ID    int64
    Roles []*Role
}

func (u User) RolesRelation() zorm.BelongsToMany[Role] {
    return zorm.BelongsToMany[Role]{
        PivotTable: "role_user",   // Join table
        ForeignKey: "user_id",     // FK in pivot table
        RelatedKey: "role_id",     // Related FK in pivot table
    }
}

Polymorphic Relations

type Image struct {
    ID            int64
    URL           string
    ImageableType string  // "users" or "posts"
    ImageableID   int64
}

// MorphOne: User has one Image
func (u User) AvatarRelation() zorm.MorphOne[Image] {
    return zorm.MorphOne[Image]{
        Type: "ImageableType",  // Type column
        ID:   "ImageableID",    // ID column
    }
}

// MorphMany: Post has many Images
func (p Post) ImagesRelation() zorm.MorphMany[Image] {
    return zorm.MorphMany[Image]{
        Type: "ImageableType",
        ID:   "ImageableID",
    }
}

// Loading with type constraints
images, _ := zorm.New[Image]().WithMorph("Imageable", map[string][]string{
    "users": {"Profile"},  // When type=users, also load Profile
    "posts": {},           // When type=posts, just load Post
}).Get(ctx)

Transactions

// Function-based transaction
err := zorm.Transaction(ctx, func(tx *zorm.Tx) error {
    user := &User{Name: "John"}
    if err := zorm.New[User]().WithTx(tx).Create(ctx, user); err != nil {
        return err // Rollback
    }

    post := &Post{UserID: user.ID, Title: "First Post"}
    if err := zorm.New[Post]().WithTx(tx).Create(ctx, post); err != nil {
        return err // Rollback
    }

    return nil // Commit
})

// Model-based transaction
err = zorm.New[User]().Transaction(ctx, func(tx *zorm.Tx) error {
    return zorm.New[User]().WithTx(tx).Create(ctx, &User{Name: "Jane"})
})

Transaction features:

  • Auto-rollback on error return
  • Auto-rollback on panic (re-panics after rollback)
  • Auto-commit on nil return

Error Handling

ZORM provides comprehensive error handling with categorized errors.

Sentinel Errors

import "github.com/rezakhademix/zorm"

// Query errors
zorm.ErrRecordNotFound     // No matching record

// Model errors
zorm.ErrInvalidModel       // Invalid model type
zorm.ErrNilPointer         // Nil pointer passed

// Relation errors
zorm.ErrRelationNotFound   // Relation method not found
zorm.ErrInvalidRelation    // Invalid relation type

// Constraint violations
zorm.ErrDuplicateKey       // Unique constraint violation
zorm.ErrForeignKey         // Foreign key constraint violation
zorm.ErrNotNullViolation   // NOT NULL constraint violation
zorm.ErrCheckViolation     // CHECK constraint violation

// Connection errors
zorm.ErrConnectionFailed   // Connection refused
zorm.ErrConnectionLost     // Connection lost during operation
zorm.ErrTimeout            // Operation timeout

// Transaction errors
zorm.ErrTransactionDeadlock    // Deadlock detected
zorm.ErrSerializationFailure  // Serialization failure

// Schema errors
zorm.ErrColumnNotFound     // Column doesn't exist
zorm.ErrTableNotFound      // Table doesn't exist
zorm.ErrInvalidSyntax      // SQL syntax error

Error Helper Functions

user, err := zorm.New[User]().Find(ctx, 999)

// Check specific error types
if zorm.IsNotFound(err) {
    // Handle not found
}

if zorm.IsDuplicateKey(err) {
    // Handle duplicate
}

if zorm.IsConstraintViolation(err) {
    // Any constraint violation
}

if zorm.IsConnectionError(err) {
    // Connection failed or lost
}

if zorm.IsTimeout(err) {
    // Operation timed out
}

if zorm.IsDeadlock(err) {
    // Transaction deadlock - retry
}

if zorm.IsSchemaError(err) {
    // Missing column, table, or syntax error
}

QueryError Details

user, err := zorm.New[User]().Create(ctx, &User{Email: "[email protected]"})
if err != nil {
    if qe := zorm.GetQueryError(err); qe != nil {
        fmt.Println(qe.Query)      // The SQL that failed
        fmt.Println(qe.Args)       // Query arguments
        fmt.Println(qe.Operation)  // "INSERT", "SELECT", etc.
        fmt.Println(qe.Table)      // Table name (if detected)
        fmt.Println(qe.Constraint) // Constraint name (if detected)
    }
}

Advanced Features

Statement Caching

Improve performance by reusing prepared statements:

cache := zorm.NewStmtCache(100)  // Cache up to 100 statements
defer cache.Close()

model := zorm.New[User]().WithStmtCache(cache)

// Statements are prepared once and reused
users, _ := model.Clone().Where("age", ">", 18).Get(ctx)
users, _ := model.Clone().Where("age", ">", 25).Get(ctx)  // Reuses prepared statement

Read/Write Splitting

// Configure resolver
zorm.ConfigureDBResolver(
    zorm.WithPrimary(primaryDB),
    zorm.WithReplicas(replica1, replica2),
    zorm.WithLoadBalancer(zorm.RoundRobinLB),
)

// Automatic routing
users, _ := zorm.New[User]().Get(ctx)          // Reads from replica
err := zorm.New[User]().Create(ctx, user)      // Writes to primary

// Force primary for consistency
users, _ := zorm.New[User]().UsePrimary().Get(ctx)

// Force specific replica
users, _ := zorm.New[User]().UseReplica(0).Get(ctx)

Common Table Expressions (CTEs)

// String CTE
users, _ := zorm.New[User]().
    WithCTE("active_users", "SELECT * FROM users WHERE active = true").
    Raw("SELECT * FROM active_users WHERE age > 18").
    Get(ctx)

// Subquery CTE
subQuery := zorm.New[User]().Where("active", true)
users, _ := zorm.New[User]().
    WithCTE("active_users", subQuery).
    Raw("SELECT * FROM active_users").
    Get(ctx)

Full-Text Search (PostgreSQL)

// Basic full-text search
articles, _ := zorm.New[Article]().
    WhereFullText("content", "database sql").Get(ctx)

// With language config
articles, _ := zorm.New[Article]().
    WhereFullTextWithConfig("content", "base de datos", "spanish").Get(ctx)

// Pre-computed tsvector column (fastest)
articles, _ := zorm.New[Article]().
    WhereTsVector("search_vector", "golang & performance").Get(ctx)

// Phrase search (word order matters)
articles, _ := zorm.New[Article]().
    WherePhraseSearch("title", "getting started").Get(ctx)

Row Locking

// Lock for update (exclusive)
user, _ := zorm.New[User]().Where("id", 1).Lock("UPDATE").First(ctx)

// Shared lock
user, _ := zorm.New[User]().Where("id", 1).Lock("SHARE").First(ctx)

// PostgreSQL-specific
user, _ := zorm.New[User]().Where("id", 1).Lock("NO KEY UPDATE").First(ctx)

Advanced Grouping

// ROLLUP
zorm.New[Order]().
    Select("region", "city", "SUM(amount)").
    GroupByRollup("region", "city").Get(ctx)

// CUBE
zorm.New[Order]().
    Select("year", "month", "SUM(amount)").
    GroupByCube("year", "month").Get(ctx)

// GROUPING SETS
zorm.New[Order]().
    GroupByGroupingSets(
        []string{"region"},
        []string{"city"},
        []string{},  // Grand total
    ).Get(ctx)

Chunking Large Datasets

err := zorm.New[User]().Chunk(ctx, 1000, func(users []*User) error {
    for _, user := range users {
        // Process each user
    }
    return nil  // Return error to stop chunking
})

Scopes (Reusable Query Logic)

func Active(q *zorm.Model[User]) *zorm.Model[User] {
    return q.Where("active", true).WhereNull("deleted_at")
}

func Verified(q *zorm.Model[User]) *zorm.Model[User] {
    return q.WhereNotNull("verified_at")
}

func RecentlyActive(q *zorm.Model[User]) *zorm.Model[User] {
    return q.Where("last_login", ">", time.Now().AddDate(0, -1, 0))
}

// Chain scopes
users, _ := zorm.New[User]().
    Scope(Active).
    Scope(Verified).
    Scope(RecentlyActive).
    Get(ctx)

Query Debugging

sql, args := zorm.New[User]().
    Where("age", ">", 18).
    OrderBy("name", "ASC").
    Limit(10).
    Print()

fmt.Println(sql)   // SELECT * FROM users WHERE 1=1 AND age > ? ORDER BY name ASC LIMIT 10
fmt.Println(args)  // [18]

Complete Example

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/rezakhademix/zorm"
)

type User struct {
    ID        int64
    Name      string
    Email     string
    Age       int
    Active    bool
    CreatedAt time.Time
    UpdatedAt time.Time
    Posts     []*Post
}

func (u *User) BeforeCreate(ctx context.Context) error {
    u.CreatedAt = time.Now()
    u.Active = true
    return nil
}

func (u User) PostsRelation() zorm.HasMany[Post] {
    return zorm.HasMany[Post]{ForeignKey: "user_id"}
}

type Post struct {
    ID        int64
    UserID    int64
    Title     string
    Published bool
}

func main() {
    ctx := context.Background()

    // Connect
    db, err := zorm.ConnectPostgres("postgres://...", nil)
    if err != nil {
        log.Fatal(err)
    }
    zorm.GlobalDB = db

    // Create with hook
    user := &User{Name: "John", Email: "[email protected]", Age: 25}
    if err := zorm.New[User]().Create(ctx, user); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Created user %d\n", user.ID)

    // Query with relations
    users, err := zorm.New[User]().
        Where("age", ">", 18).
        Where("active", true).
        WithCallback("Posts", func(q *zorm.Model[Post]) {
            q.Where("published", true).Limit(5)
        }).
        OrderBy("created_at", "DESC").
        Limit(10).
        Get(ctx)

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

    for _, u := range users {
        fmt.Printf("%s has %d published posts\n", u.Name, len(u.Posts))
    }

    // FirstOrCreate
    user, err = zorm.New[User]().FirstOrCreate(ctx,
        map[string]any{"email": "[email protected]"},
        map[string]any{"name": "Jane", "age": 30},
    )

    // Pagination
    result, _ := zorm.New[User]().Paginate(ctx, 1, 15)
    fmt.Printf("Page 1 of %d, Total: %d\n", result.LastPage, result.Total)
}

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License - see LICENSE file for details.

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrRecordNotFound Query errors
	// ErrRecordNotFound is returned when a query returns no results
	ErrRecordNotFound = errors.New("zorm: record not found")

	// ErrInvalidModel Model errors
	// ErrInvalidModel is returned when the model type is invalid
	ErrInvalidModel = errors.New("zorm: invalid model")
	// ErrNilPointer is returned when a nil pointer is passed
	ErrNilPointer = errors.New("zorm: nil pointer")

	// ErrNoContext Context errors
	// ErrNoContext is returned when no context is provided
	ErrNoContext = errors.New("zorm: no context provided")

	// ErrRelationNotFound Relation errors
	// ErrRelationNotFound is returned when a relation method is not found
	ErrRelationNotFound = errors.New("zorm: relation not found")
	// ErrInvalidRelation is returned when relation type is invalid
	ErrInvalidRelation = errors.New("zorm: invalid relation type")
	// ErrInvalidConfig is returned when relation config is invalid
	ErrInvalidConfig = errors.New("zorm: invalid relation config")

	// ErrDuplicateKey Constraint violation errors
	// ErrDuplicateKey is returned for unique constraint violations
	ErrDuplicateKey = errors.New("zorm: duplicate key violation")
	// ErrForeignKey is returned for foreign key constraint violations
	ErrForeignKey = errors.New("zorm: foreign key constraint violation")
	// ErrCheckViolation is returned for CHECK constraint violations
	ErrCheckViolation = errors.New("zorm: check constraint violation")
	// ErrNotNullViolation is returned for NOT NULL constraint violations
	ErrNotNullViolation = errors.New("zorm: not null constraint violation")

	// ErrConnectionFailed Connection errors
	// ErrConnectionFailed is returned when database connection fails
	ErrConnectionFailed = errors.New("zorm: connection failed")
	// ErrConnectionLost is returned when connection is lost during operation
	ErrConnectionLost = errors.New("zorm: connection lost")
	// ErrTimeout is returned when a query or connection times out
	ErrTimeout = errors.New("zorm: operation timeout")

	// ErrTransactionDeadlock Transaction errors
	// ErrTransactionDeadlock is returned when a deadlock is detected
	ErrTransactionDeadlock = errors.New("zorm: transaction deadlock")
	// ErrSerializationFailure is returned for serialization failures
	ErrSerializationFailure = errors.New("zorm: serialization failure")

	// ErrColumnNotFound Schema errors
	// ErrColumnNotFound is returned when a column doesn't exist
	ErrColumnNotFound = errors.New("zorm: column not found")
	// ErrTableNotFound is returned when a table doesn't exist
	ErrTableNotFound = errors.New("zorm: table not found")
	// ErrInvalidSyntax is returned for SQL syntax errors
	ErrInvalidSyntax = errors.New("zorm: invalid SQL syntax")

	// ErrRequiresRawQuery Other errors
	// ErrRequiresRawQuery is returned when operation requires raw query
	ErrRequiresRawQuery = errors.New("zorm: operation requires raw query")
)

Sentinel errors for common failure cases

View Source
var ErrInvalidColumnName = fmt.Errorf("zorm: invalid column name")

ErrInvalidColumnName is returned when a column name contains invalid characters.

View Source
var GlobalDB *sql.DB

GlobalDB is the global database connection pool. In a real app, this might be managed differently, but for this ORM style, we often have a global or a passed-in DB. For now, we'll allow setting it globally or per-instance.

Functions

func ConfigureConnectionPool

func ConfigureConnectionPool(db *sql.DB, maxOpen, maxIdle int, maxLifetime, idleTimeout time.Duration)

ConfigureConnectionPool configures the database connection pool.

func ConfigureDBResolver

func ConfigureDBResolver(opts ...ResolverOption)

ConfigureDBResolver configures the global database resolver for primary/replica setup. Example:

ConfigureDBResolver(
    WithPrimary(primaryDB),
    WithReplicas(replica1, replica2),
    WithLoadBalancer(RoundRobinLB),
)

func ConnectPostgres

func ConnectPostgres(dsn string, config *DBConfig) (*sql.DB, error)

ConnectPostgres creates a new *sql.DB connection pool for PostgreSQL using pgx driver. dsn: "postgres://user:password@host:port/dbname?sslmode=disable"

func GetStringBuilder

func GetStringBuilder() *strings.Builder

GetStringBuilder retrieves a strings.Builder from the pool.

func IsCheckViolation

func IsCheckViolation(err error) bool

IsCheckViolation checks if the error is a CHECK constraint violation.

func IsConnectionError

func IsConnectionError(err error) bool

IsConnectionError checks if the error is a connection failure. This includes both connection refused and connection lost errors.

func IsConstraintViolation

func IsConstraintViolation(err error) bool

IsConstraintViolation checks if the error is any type of constraint violation. This includes unique, foreign key, not null, and check constraints.

func IsDeadlock

func IsDeadlock(err error) bool

IsDeadlock checks if the error is a transaction deadlock.

func IsDuplicateKey

func IsDuplicateKey(err error) bool

IsDuplicateKey checks if the error is a duplicate key violation.

func IsForeignKeyViolation

func IsForeignKeyViolation(err error) bool

IsForeignKeyViolation checks if the error is a foreign key violation.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound checks if the error is ErrRecordNotFound. It uses errors.Is to check the error chain.

func IsNotNullViolation

func IsNotNullViolation(err error) bool

IsNotNullViolation checks if the error is a NOT NULL constraint violation.

func IsSchemaError

func IsSchemaError(err error) bool

IsSchemaError checks if the error is a schema-related error. This includes missing columns, missing tables, and syntax errors.

func IsSerializationFailure

func IsSerializationFailure(err error) bool

IsSerializationFailure checks if the error is a serialization failure.

func IsTimeout

func IsTimeout(err error) bool

IsTimeout checks if the error is a timeout error.

func MustValidateColumnName added in v0.5.0

func MustValidateColumnName(name string)

MustValidateColumnName validates a column name and panics if invalid. Use this for internal validation where invalid column names indicate programming errors.

func PutStringBuilder

func PutStringBuilder(sb *strings.Builder)

PutStringBuilder returns a strings.Builder to the pool after resetting it.

func ToSnakeCase

func ToSnakeCase(s string) string

ToSnakeCase converts a string to snake_case. Handles acronyms correctly (e.g., UserID -> user_id, HTTPClient -> http_client). Results are cached to avoid repeated conversions for the same input.

func Transaction

func Transaction(ctx context.Context, fn func(tx *Tx) error) error

Transaction executes a function within a transaction.

func ValidateColumnName added in v0.5.0

func ValidateColumnName(name string) error

ValidateColumnName checks if a column name is safe to use in SQL queries. It uses a strict whitelist approach to prevent SQL injection. Allowed characters: alphanumeric, underscore, dot, asterisk, space, parens, comma. Dangerous characters like quotes, semicolons, and comments are rejected.

func WrapQueryError

func WrapQueryError(operation, query string, args []any, err error) error

WrapQueryError wraps a database error with query context. It analyzes the error to categorize it and extract relevant information such as table names and constraint names where possible.

func WrapRelationError

func WrapRelationError(relation, modelType string, err error) error

WrapRelationError wraps a relation error with context

Types

type BelongsTo

type BelongsTo[T any] struct {
	ForeignKey string
	OwnerKey   string
	Table      string
}

BelongsTo defines a BelongsTo relation.

func (BelongsTo[T]) GetOverrideTable

func (r BelongsTo[T]) GetOverrideTable() string

func (BelongsTo[T]) NewModel added in v0.6.0

func (BelongsTo[T]) NewModel(ctx context.Context, db *sql.DB) any

func (BelongsTo[T]) NewRelated

func (BelongsTo[T]) NewRelated() any

func (BelongsTo[T]) RelationType

func (BelongsTo[T]) RelationType() RelationType

type BelongsToMany

type BelongsToMany[T any] struct {
	PivotTable string
	ForeignKey string
	RelatedKey string
	LocalKey   string
	RelatedPK  string
	Table      string
}

BelongsToMany defines a BelongsToMany relation.

func (BelongsToMany[T]) NewModel added in v0.6.0

func (BelongsToMany[T]) NewModel(ctx context.Context, db *sql.DB) any

func (BelongsToMany[T]) NewRelated

func (BelongsToMany[T]) NewRelated() any

func (BelongsToMany[T]) RelationType

func (BelongsToMany[T]) RelationType() RelationType

type CTE

type CTE struct {
	Name  string
	Query any // string or *Model[T]
	Args  []any
}

CTE represents a Common Table Expression.

type Cursor

type Cursor[T any] struct {
	// contains filtered or unexported fields
}

Cursor provides a typed, forward-only iterator over database query results. It wraps sql.Rows and maps each row into the generic model type T.

func (*Cursor[T]) Close

func (c *Cursor[T]) Close() error

Close closes the cursor.

func (*Cursor[T]) Next

func (c *Cursor[T]) Next() bool

Next prepares the next result row for reading with the Scan method.

func (*Cursor[T]) Scan

func (c *Cursor[T]) Scan(ctx context.Context) (*T, error)

Scan scans the current row into a new entity.

type DBConfig

type DBConfig struct {
	MaxOpenConns    int
	MaxIdleConns    int
	ConnMaxLifetime time.Duration
	ConnMaxIdleTime time.Duration
}

DBConfig configures the connection pool settings.

type DBResolver

type DBResolver struct {
	// contains filtered or unexported fields
}

DBResolver manages primary and replica database connections. It automatically routes write operations to the primary and read operations to replicas.

var GlobalResolver *DBResolver

GlobalResolver is the global database resolver for primary/replica setup. If configured, it will automatically route read queries to replicas and write queries to the primary database.

func (*DBResolver) HasReplicas

func (r *DBResolver) HasReplicas() bool

HasReplicas returns true if replicas are configured.

func (*DBResolver) Primary

func (r *DBResolver) Primary() *sql.DB

Primary returns the primary database connection.

func (*DBResolver) Replica

func (r *DBResolver) Replica() *sql.DB

Replica returns a replica based on the load balancer strategy.

func (*DBResolver) ReplicaAt

func (r *DBResolver) ReplicaAt(index int) *sql.DB

ReplicaAt returns a specific replica by index. Returns nil if index is out of bounds.

type FieldInfo

type FieldInfo struct {
	Name      string // Struct field name
	Column    string // DB column name
	IsPrimary bool
	IsAuto    bool // Auto-increment or managed
	FieldType reflect.Type
	Index     []int // Index path for nested fields (if we support embedding)
}

FieldInfo holds data about a single field in the model.

type HasMany

type HasMany[T any] struct {
	ForeignKey string
	LocalKey   string
	Table      string
}

HasMany defines a HasMany relation.

func (HasMany[T]) GetOverrideTable

func (r HasMany[T]) GetOverrideTable() string

func (HasMany[T]) NewModel added in v0.6.0

func (HasMany[T]) NewModel(ctx context.Context, db *sql.DB) any

func (HasMany[T]) NewRelated

func (HasMany[T]) NewRelated() any

func (HasMany[T]) RelationType

func (HasMany[T]) RelationType() RelationType

type HasOne

type HasOne[T any] struct {
	ForeignKey string
	LocalKey   string
	Table      string
}

HasOne defines a HasOne relation.

func (HasOne[T]) GetOverrideTable

func (r HasOne[T]) GetOverrideTable() string

func (HasOne[T]) NewModel added in v0.6.0

func (HasOne[T]) NewModel(ctx context.Context, db *sql.DB) any

func (HasOne[T]) NewRelated

func (HasOne[T]) NewRelated() any

func (HasOne[T]) RelationType

func (HasOne[T]) RelationType() RelationType

type LoadBalancer

type LoadBalancer interface {
	Next(replicas []*sql.DB) *sql.DB
}

LoadBalancer is an interface for selecting a replica from a pool.

var RandomLB LoadBalancer = &RandomLoadBalancer{}

RandomLB is a convenience variable for random load balancing.

var RoundRobinLB LoadBalancer = &RoundRobinLoadBalancer{}

RoundRobinLB is a convenience variable for round-robin load balancing.

type Model

type Model[T any] struct {
	// contains filtered or unexported fields
}

Model provides a strongly typed ORM interface for working with the entity type T. It stores the active query state—including selected columns, filters, ordering, grouping, relation loading rules, and raw SQL segments—allowing the builder to compose complex queries in a structured and chainable manner.

The Model also tracks the execution context, database handle or transaction, and metadata derived from T that is used for mapping database rows into entities.

func New

func New[T any]() *Model[T]

New creates a new Model instance for type T.

func (*Model[T]) Attach

func (m *Model[T]) Attach(ctx context.Context, entity *T, relation string, ids []any, pivotData map[any]map[string]any) error

Attach inserts rows into the pivot table for a BelongsToMany relation. pivotData: map[any]map[string]any (RelatedID -> {Column: Value})

func (*Model[T]) Avg

func (m *Model[T]) Avg(ctx context.Context, column string) (float64, error)

Avg calculates the average of a column. Returns 0 if no rows match or the average is null.

func (*Model[T]) Chunk

func (m *Model[T]) Chunk(ctx context.Context, size int, callback func([]*T) error) error

Chunk processes the results in chunks to save memory.

func (*Model[T]) Clone

func (m *Model[T]) Clone() *Model[T]

Clone creates a deep copy of the Model. This is useful for creating new queries based on an existing one without modifying it.

func (*Model[T]) Count

func (m *Model[T]) Count(ctx context.Context) (int64, error)

Count returns the number of records matching the query.

func (*Model[T]) CountOver

func (m *Model[T]) CountOver(ctx context.Context, column string) (map[any]int64, error)

CountOver returns count of records partitioned by the specified column. This uses window functions: COUNT(*) OVER (PARTITION BY column). Returns a map of column value -> count.

func (*Model[T]) Create

func (m *Model[T]) Create(ctx context.Context, entity *T) error

Create inserts a new record.

func (*Model[T]) CreateMany

func (m *Model[T]) CreateMany(ctx context.Context, entities []*T) error

CreateMany inserts multiple records in a single query.

func (*Model[T]) Cursor

func (m *Model[T]) Cursor(ctx context.Context) (*Cursor[T], error)

Cursor returns a cursor for iterating over results one by one. Useful for large datasets to avoid loading everything into memory.

func (*Model[T]) Delete

func (m *Model[T]) Delete(ctx context.Context) error

Delete deletes records matching the current query conditions. WARNING: Without WHERE conditions, this will delete ALL records in the table.

func (*Model[T]) DeleteMany

func (m *Model[T]) DeleteMany(ctx context.Context) error

DeleteMany deletes records matching the query.

func (*Model[T]) Detach

func (m *Model[T]) Detach(ctx context.Context, entity *T, relation string, ids []any) error

Detach deletes rows from the pivot table.

func (*Model[T]) Distinct

func (m *Model[T]) Distinct() *Model[T]

Distinct adds DISTINCT to the SELECT clause to return only unique rows.

func (*Model[T]) DistinctBy

func (m *Model[T]) DistinctBy(columns ...string) *Model[T]

DistinctBy adds DISTINCT ON (columns) to the SELECT clause. This is a PostgreSQL-specific feature that returns the first row of each set of rows where the given columns match.

func (*Model[T]) Exec

func (m *Model[T]) Exec(ctx context.Context) (sql.Result, error)

Exec executes the query (Raw or Builder) and returns the result.

func (*Model[T]) Exists added in v0.4.0

func (m *Model[T]) Exists(ctx context.Context) (bool, error)

Exists checks if any record matches the query conditions. It uses "SELECT 1 FROM table WHERE conditions LIMIT 1" for efficiency.

func (*Model[T]) Find

func (m *Model[T]) Find(ctx context.Context, id any) (*T, error)

Find finds a record by ID.

func (*Model[T]) FindOrFail

func (m *Model[T]) FindOrFail(ctx context.Context, id any) (*T, error)

FindOrFail finds a record by ID or returns an error. In Go, this is identical to Find, but added for API parity.

func (*Model[T]) First

func (m *Model[T]) First(ctx context.Context) (*T, error)

First executes the query and returns the first result.

func (*Model[T]) FirstOrCreate

func (m *Model[T]) FirstOrCreate(ctx context.Context, attributes map[string]any, values map[string]any) (*T, error)

FirstOrCreate finds the first record matching attributes or creates it with attributes+values. If found, returns the existing record. If not found, creates a new record with merged attributes+values.

func (*Model[T]) Get

func (m *Model[T]) Get(ctx context.Context) ([]*T, error)

Get executes the query and returns a slice of results.

func (*Model[T]) GetArgs

func (m *Model[T]) GetArgs() []any

GetArgs returns the arguments.

func (*Model[T]) GetWheres

func (m *Model[T]) GetWheres() []string

GetWheres returns the where clauses.

func (*Model[T]) GroupBy

func (m *Model[T]) GroupBy(columns ...string) *Model[T]

GroupBy adds a GROUP BY clause. Column names are validated to prevent SQL injection.

func (*Model[T]) GroupByCube

func (m *Model[T]) GroupByCube(columns ...string) *Model[T]

GroupByCube adds a GROUP BY CUBE clause.

func (*Model[T]) GroupByGroupingSets

func (m *Model[T]) GroupByGroupingSets(sets ...[]string) *Model[T]

GroupByGroupingSets adds a GROUP BY GROUPING SETS clause. Each slice in sets represents a grouping set. Empty slice represents empty grouping set ().

func (*Model[T]) GroupByRollup

func (m *Model[T]) GroupByRollup(columns ...string) *Model[T]

GroupByRollup adds a GROUP BY ROLLUP clause.

func (*Model[T]) Having

func (m *Model[T]) Having(query string, args ...any) *Model[T]

Having adds a HAVING clause (used with GROUP BY).

func (*Model[T]) Latest

func (m *Model[T]) Latest(columns ...string) *Model[T]

Latest adds an ORDER BY column DESC clause. Defaults to "created_at".

func (*Model[T]) Limit

func (m *Model[T]) Limit(n int) *Model[T]

Limit sets the LIMIT clause.

func (*Model[T]) Load

func (m *Model[T]) Load(ctx context.Context, entity *T, relations ...string) error

Load eager loads relations on a single entity.

func (*Model[T]) LoadMorph

func (m *Model[T]) LoadMorph(ctx context.Context, entities []*T, relation string, typeMap map[string][]string) error

LoadMorph eager loads a polymorphic relation with constraints on a slice.

func (*Model[T]) LoadSlice

func (m *Model[T]) LoadSlice(ctx context.Context, entities []*T, relations ...string) error

LoadSlice eager loads relations on a slice of entities.

func (*Model[T]) Lock

func (m *Model[T]) Lock(mode string) *Model[T]

Lock adds a locking clause to the SELECT query. Common modes: "UPDATE", "NO KEY UPDATE", "SHARE", "KEY SHARE" This will generate: SELECT ... FOR [mode]

func (*Model[T]) Offset

func (m *Model[T]) Offset(n int) *Model[T]

Offset sets the OFFSET clause.

func (*Model[T]) Oldest

func (m *Model[T]) Oldest(columns ...string) *Model[T]

Oldest adds an ORDER BY column ASC clause. Defaults to "created_at".

func (*Model[T]) OrWhere

func (m *Model[T]) OrWhere(query any, args ...any) *Model[T]

OrWhere adds an OR WHERE clause.

func (*Model[T]) OrWhereNotNull

func (m *Model[T]) OrWhereNotNull(column string) *Model[T]

OrWhereNotNull adds an OR condition that checks whether the given column is NOT NULL.

Example:

Model[User]().OrWhereNotNull("verified_at")
// OR verified_at IS NOT NULL

func (*Model[T]) OrWhereNull

func (m *Model[T]) OrWhereNull(column string) *Model[T]

OrWhereNull adds an OR condition that checks whether the given column is NULL.

Example:

Model[User]().OrWhereNull("deleted_at")
// OR deleted_at IS NULL

func (*Model[T]) OrderBy

func (m *Model[T]) OrderBy(column, direction string) *Model[T]

OrderBy adds an ORDER BY clause. Column names are validated to prevent SQL injection.

func (*Model[T]) Paginate

func (m *Model[T]) Paginate(ctx context.Context, page, perPage int) (*PaginationResult[T], error)

Paginate executes the query with pagination. If page is less than 1, it defaults to 1. If perPage is less than 1, it defaults to 15.

func (*Model[T]) Pluck

func (m *Model[T]) Pluck(ctx context.Context, column string) ([]any, error)

Pluck retrieves a single column's values from the result set.

func (*Model[T]) Print

func (m *Model[T]) Print() (string, []any)

Print returns the SQL query and arguments that would be executed without running it. This is useful for debugging and logging the generated SQL. Example:

sql, args := m.Where("status", "active").Limit(10).Print()
fmt.Println(sql, args)

Output: "SELECT * FROM users WHERE 1=1 AND (status = $1) LIMIT 10" [active]

Example

Example usage for documentation

m := New[TestModel]()
m.Where("status", "active").Limit(10)

sql, args := m.Print()
fmt.Println(sql)
fmt.Println(args)
Output:

SELECT * FROM test_models WHERE 1=1  AND status = $1 LIMIT 10
[active]

func (*Model[T]) Raw

func (m *Model[T]) Raw(query string, args ...any) *Model[T]

Raw sets a raw SQL query and arguments.

func (*Model[T]) Scope

func (m *Model[T]) Scope(fn func(*Model[T]) *Model[T]) *Model[T]

Scope applies a function to the query builder. Useful for reusable query logic (Scopes).

func (*Model[T]) Select

func (m *Model[T]) Select(columns ...string) *Model[T]

Select specifies which columns to select. Column names are validated to prevent SQL injection.

func (*Model[T]) SetDB

func (m *Model[T]) SetDB(db *sql.DB) *Model[T]

SetDB sets a custom database connection for this model instance.

func (*Model[T]) SimplePaginate

func (m *Model[T]) SimplePaginate(ctx context.Context, page, perPage int) (*PaginationResult[T], error)

SimplePaginate executes the query with pagination but skips the count query. Use this when you don't need the total count (e.g., "Load More" buttons). This is ~2x faster than Paginate() since it only runs 1 query. If page is less than 1, it defaults to 1. If perPage is less than 1, it defaults to 15.

func (*Model[T]) Sum

func (m *Model[T]) Sum(ctx context.Context, column string) (float64, error)

Sum calculates the sum of a column. Returns 0 if no rows match or the sum is null.

func (*Model[T]) Sync

func (m *Model[T]) Sync(ctx context.Context, entity *T, relation string, ids []any, pivotData map[any]map[string]any) error

Sync synchronizes the association with the given IDs. It attaches missing IDs and detaches IDs that are not in the new list. pivotData: map[any]map[string]any (RelatedID -> {Column: Value})

func (*Model[T]) Table

func (m *Model[T]) Table(name string) *Model[T]

Table sets a custom table name for the query. This overrides the table name derived from the struct type.

func (*Model[T]) TableName

func (m *Model[T]) TableName() string

TableName returns the table name for the model. If a custom table name is set via Table(), it returns that. Otherwise, it returns the table name from the model info.

func (*Model[T]) Transaction

func (m *Model[T]) Transaction(ctx context.Context, fn func(tx *Tx) error) error

Transaction executes a function within a transaction using the model's database connection.

func (*Model[T]) Update

func (m *Model[T]) Update(ctx context.Context, entity *T) error

Update updates a single record based on its primary key. The entity must not be nil and must have a valid primary key value.

func (*Model[T]) UpdateMany

func (m *Model[T]) UpdateMany(ctx context.Context, values map[string]any) error

UpdateMany updates records matching the query with values.

func (*Model[T]) UpdateOrCreate

func (m *Model[T]) UpdateOrCreate(ctx context.Context, attributes map[string]any, values map[string]any) (*T, error)

UpdateOrCreate finds a record matching attributes and updates it with values, or creates it. If found, updates the record with values. If not found, creates a new record with merged attributes+values.

func (*Model[T]) UsePrimary

func (m *Model[T]) UsePrimary() *Model[T]

UsePrimary forces the next query to use the primary database connection. This is useful when you need to read from primary for consistency, such as immediately after a write operation. Example: m.UsePrimary().Get()

func (*Model[T]) UseReplica

func (m *Model[T]) UseReplica(index int) *Model[T]

UseReplica forces the next query to use a specific replica by index. This is useful for testing or when you want to target a specific replica. Example: m.UseReplica(0).Get()

func (*Model[T]) Where

func (m *Model[T]) Where(query any, args ...any) *Model[T]

Where adds a WHERE clause. Supports multiple forms:

Where("column", value) -> column = ? (converted to $n at execution)
Where("column", ">", value) -> column > ?
Where(map[string]any{"name": "John", "age": 30}) -> name = ? AND age = ?
Where(&User{Name: "John"}) -> name = ?
Where(func(q *Model[T]) { ... }) -> nested group with parentheses

func (*Model[T]) WhereFullText

func (m *Model[T]) WhereFullText(column, searchText string) *Model[T]

WhereFullText adds a full-text search condition using tsvector and tsquery. Uses default 'english' configuration and plainto_tsquery for user-friendly search. Example: WhereFullText("content", "search terms") Generates: WHERE to_tsvector('english', content) @@ plainto_tsquery('english', ?)

func (*Model[T]) WhereFullTextWithConfig

func (m *Model[T]) WhereFullTextWithConfig(column, searchText, config string) *Model[T]

WhereFullTextWithConfig adds a full-text search condition with a custom text search configuration. Example: WhereFullTextWithConfig("content", "search terms", "spanish") Generates: WHERE to_tsvector('spanish', content) @@ plainto_tsquery('spanish', ?)

func (*Model[T]) WhereHas

func (m *Model[T]) WhereHas(relation string, callback any) *Model[T]

WhereHas adds a WHERE EXISTS clause for a relation.

func (*Model[T]) WhereIn

func (m *Model[T]) WhereIn(column string, args []any) *Model[T]

WhereIn adds a WHERE IN clause. Column names are validated to prevent SQL injection.

func (*Model[T]) WhereNotNull

func (m *Model[T]) WhereNotNull(column string) *Model[T]

WhereNotNull adds an AND condition that checks whether the given column is NOT NULL.

Example:

Model[User]().WhereNotNull("verified_at")
// WHERE verified_at IS NOT NULL

func (*Model[T]) WhereNull

func (m *Model[T]) WhereNull(column string) *Model[T]

WhereNull adds an AND condition that checks whether the given column is NULL.

Example:

Model[User]().WhereNull("deleted_at")
// WHERE deleted_at IS NULL

func (*Model[T]) WherePhraseSearch

func (m *Model[T]) WherePhraseSearch(column, phrase string) *Model[T]

WherePhraseSearch adds an exact phrase search condition. Uses phraseto_tsquery which preserves word order. Example: WherePhraseSearch("content", "fat cat") Generates: WHERE to_tsvector('english', content) @@ phraseto_tsquery('english', ?)

func (*Model[T]) WhereTsVector

func (m *Model[T]) WhereTsVector(tsvectorColumn, tsquery string) *Model[T]

WhereTsVector adds a full-text search condition on a pre-computed tsvector column. This is more efficient when you have an indexed tsvector column. Example: WhereTsVector("search_vector", "fat & rat") Generates: WHERE search_vector @@ to_tsquery('english', ?)

func (*Model[T]) With

func (m *Model[T]) With(relations ...string) *Model[T]

With adds relations to eager load. Multiple relation names can be specified, including nested relations.

Examples:

With("Posts")                    // Single relation
With("Posts", "Comments")        // Multiple relations
With("Posts.Comments")           // Nested relation

func (*Model[T]) WithCTE

func (m *Model[T]) WithCTE(name string, query any) *Model[T]

WithCTE adds a Common Table Expression (CTE) to the query. query can be a string or a *Model[T].

func (*Model[T]) WithCallback

func (m *Model[T]) WithCallback(relation string, callback any) *Model[T]

WithCallback adds a relation with a callback to apply constraints. The callback receives a query builder for the related model and can apply filters, ordering, limits, etc.

Example:

WithCallback("Posts", func(q *Model[Post]) {
    q.Where("published", true).OrderBy("created_at", "DESC").Limit(10)
})

func (*Model[T]) WithContext

func (m *Model[T]) WithContext(ctx context.Context) *Model[T]

WithContext sets the context for the query.

func (*Model[T]) WithMorph

func (m *Model[T]) WithMorph(relation string, typeMap map[string][]string) *Model[T]

WithMorph adds a polymorphic relation to eager load with type-specific constraints. typeMap: map[string][]string{"events": {"Calendar"}, "posts": {"Author"}}

func (*Model[T]) WithStmtCache

func (m *Model[T]) WithStmtCache(cache *StmtCache) *Model[T]

WithStmtCache enables statement caching for this model instance. The cache will be used to store and reuse prepared statements, improving performance by avoiding re-preparation of frequently used queries.

Example:

cache := NewStmtCache(100)
defer cache.Close()
model := New[User]().WithStmtCache(cache)

func (*Model[T]) WithTx

func (m *Model[T]) WithTx(tx *Tx) *Model[T]

WithTx sets the transaction for the model.

type ModelInfo

type ModelInfo struct {
	Type            reflect.Type
	TableName       string
	PrimaryKey      string
	Fields          map[string]*FieldInfo // StructFieldName -> FieldInfo
	Columns         map[string]*FieldInfo // DBColumnName -> FieldInfo
	Accessors       []int                 // Indices of methods starting with "Get"
	RelationMethods map[string]int        // MethodName -> Index
}

ModelInfo holds the reflection data for a model struct.

func ParseModel

func ParseModel[T any]() *ModelInfo

ParseModel inspects the struct T and returns its metadata.

func ParseModelType

func ParseModelType(typ reflect.Type) *ModelInfo

ParseModelType inspects the type and returns its metadata.

type MorphMany

type MorphMany[T any] struct {
	Type  string // Column name in related table (e.g. imageable_type)
	ID    string // Column name in related table (e.g. imageable_id)
	Table string
}

MorphMany defines a polymorphic HasMany relation.

func (MorphMany[T]) GetOverrideTable

func (r MorphMany[T]) GetOverrideTable() string

func (MorphMany[T]) NewModel added in v0.6.0

func (MorphMany[T]) NewModel(ctx context.Context, db *sql.DB) any

func (MorphMany[T]) NewRelated

func (MorphMany[T]) NewRelated() any

func (MorphMany[T]) RelationType

func (MorphMany[T]) RelationType() RelationType

type MorphOne

type MorphOne[T any] struct {
	Type  string // Column name in related table (e.g. imageable_type)
	ID    string // Column name in related table (e.g. imageable_id)
	Table string
}

MorphOne defines a polymorphic HasOne relation.

func (MorphOne[T]) GetOverrideTable

func (r MorphOne[T]) GetOverrideTable() string

func (MorphOne[T]) NewModel added in v0.6.0

func (MorphOne[T]) NewModel(ctx context.Context, db *sql.DB) any

func (MorphOne[T]) NewRelated

func (MorphOne[T]) NewRelated() any

func (MorphOne[T]) RelationType

func (MorphOne[T]) RelationType() RelationType

type MorphTo

type MorphTo[T any] struct {
	Type    string         // Column name for Type (e.g. imageable_type)
	ID      string         // Column name for ID (e.g. imageable_id)
	TypeMap map[string]any // Map of DB type string to empty struct instance (e.g. "posts": Post{})
}

MorphTo defines a polymorphic BelongsTo relation. T is usually `any` or a common interface, but in our generic system, the field in the struct will likely be `any` or an interface. However, `Relation` interface requires `NewRelated()`. For MorphTo, `NewRelated` is dynamic. We might need a special handling for MorphTo.

func (MorphTo[T]) NewModel added in v0.6.0

func (MorphTo[T]) NewModel(ctx context.Context, db *sql.DB) any

func (MorphTo[T]) NewRelated

func (MorphTo[T]) NewRelated() any

func (MorphTo[T]) RelationType

func (MorphTo[T]) RelationType() RelationType

type PaginationResult

type PaginationResult[T any] struct {
	Data        []*T  `json:"data"`
	Total       int64 `json:"total"`
	PerPage     int   `json:"per_page"`
	CurrentPage int   `json:"current_page"`
	LastPage    int   `json:"last_page"`
}

PaginationResult holds pagination metadata and data.

type QueryError

type QueryError struct {
	Query      string // The SQL query that failed
	Args       []any  // The query arguments
	Operation  string // Operation type: SELECT, INSERT, UPDATE, DELETE, etc.
	Err        error  // The underlying error
	Table      string // The table involved (if detectable)
	Constraint string // The constraint name (if constraint violation)
}

QueryError wraps database errors with query context for better debugging. It provides detailed information about what went wrong including the query, arguments, operation type, and optionally the affected table and constraint.

func GetQueryError

func GetQueryError(err error) *QueryError

GetQueryError extracts the underlying QueryError from an error if present. Returns nil if the error is not or does not wrap a QueryError. Use this to access query details like the SQL, args, table, and constraint.

func (*QueryError) Error

func (e *QueryError) Error() string

func (*QueryError) Unwrap

func (e *QueryError) Unwrap() error

type RandomLoadBalancer

type RandomLoadBalancer struct{}

RandomLoadBalancer is a placeholder for future random load balancing. Currently not implemented, defaults to round-robin.

func (*RandomLoadBalancer) Next

func (r *RandomLoadBalancer) Next(replicas []*sql.DB) *sql.DB

type Relation

type Relation interface {
	RelationType() RelationType
	NewRelated() any
	NewModel(ctx context.Context, db *sql.DB) any
}

Relation interface allows us to handle generics uniformly.

type RelationDefinition

type RelationDefinition struct {
	Type        RelationType
	Field       string // The struct field name in the parent model
	RelatedType reflect.Type

	// Keys
	ForeignKey string
	LocalKey   string
	OwnerKey   string // For BelongsTo

	// Pivot (BelongsToMany)
	PivotTable   string
	PivotForeign string
	PivotRelated string
}

RelationDefinition holds metadata about a relation.

type RelationError

type RelationError struct {
	Relation  string // Name of the relation
	ModelType string // Type of the model
	Err       error  // The underlying error
}

RelationError wraps relation loading failures with context

func (*RelationError) Error

func (e *RelationError) Error() string

func (*RelationError) Unwrap

func (e *RelationError) Unwrap() error

type RelationType

type RelationType string

RelationType defines the type of relationship between two models in the ORM.

const (
	// RelationHasOne represents a one-to-one relationship where the current
	// model owns a single related record.
	RelationHasOne RelationType = "HasOne"

	// RelationHasMany represents a one-to-many relationship where the current
	// model owns multiple related records.
	RelationHasMany RelationType = "HasMany"

	// RelationBelongsTo represents an inverse one-to-one or one-to-many
	// relationship where the current model references a parent record.
	RelationBelongsTo RelationType = "BelongsTo"

	// RelationBelongsToMany represents a many-to-many relationship between
	// two models, typically connected through a join table.
	RelationBelongsToMany RelationType = "BelongsToMany"
)
const (
	// RelationMorphTo represents a polymorphic inverse relationship where the
	// current model can belong to one of several different model types. The
	// actual target type and ID are determined by discriminator columns such as
	// "morph_type" and "morph_id".
	RelationMorphTo RelationType = "MorphTo"

	// RelationMorphOne represents a polymorphic one-to-one relationship where a
	// single related record can be associated with multiple possible parent
	// model types.
	RelationMorphOne RelationType = "MorphOne"

	// RelationMorphMany represents a polymorphic one-to-many relationship where
	// multiple related records can be associated with various parent model types.
	RelationMorphMany RelationType = "MorphMany"
)

type ResolverOption

type ResolverOption func(*DBResolver)

ResolverOption is a functional option for configuring DBResolver.

func WithLoadBalancer

func WithLoadBalancer(lb LoadBalancer) ResolverOption

WithLoadBalancer sets the load balancer strategy. Default is RoundRobinLoadBalancer.

func WithPrimary

func WithPrimary(db *sql.DB) ResolverOption

WithPrimary sets the primary database connection.

func WithReplicas

func WithReplicas(dbs ...*sql.DB) ResolverOption

WithReplicas sets the replica database connections.

type RoundRobinLoadBalancer

type RoundRobinLoadBalancer struct {
	// contains filtered or unexported fields
}

RoundRobinLoadBalancer distributes load across replicas using round-robin.

func (*RoundRobinLoadBalancer) Next

func (r *RoundRobinLoadBalancer) Next(replicas []*sql.DB) *sql.DB

Next returns the next replica in round-robin order.

type StmtCache

type StmtCache struct {
	// contains filtered or unexported fields
}

StmtCache provides a thread-safe LRU cache for prepared statements. It stores prepared SQL statements and automatically evicts the least recently used entries when the cache reaches its maximum capacity.

The cache is safe for concurrent use by multiple goroutines and helps improve performance by reusing prepared statements instead of re-preparing them on every execution.

func NewStmtCache

func NewStmtCache(capacity int) *StmtCache

NewStmtCache creates a new statement cache with the specified capacity. When the cache reaches capacity, the least recently used statement will be evicted to make room for new entries.

A capacity of 0 or negative value will default to 100.

func (*StmtCache) Clear

func (c *StmtCache) Clear()

Clear closes all cached statements and clears the cache.

func (*StmtCache) Close

func (c *StmtCache) Close() error

Close closes all cached statements and releases all resources.

func (*StmtCache) Get

func (c *StmtCache) Get(query string) (*sql.Stmt, func())

Get retrieves a cached prepared statement for the given SQL query. Returns the statement and a release function. The caller MUST call the release function when finished using the statement. Returns nil, nil if the statement is not found in the cache.

func (*StmtCache) Len

func (c *StmtCache) Len() int

Len returns the current number of cached statements.

func (*StmtCache) Put

func (c *StmtCache) Put(query string, stmt *sql.Stmt)

Put stores a prepared statement in the cache for the given SQL query. If the cache is at capacity, the least recently used statement will be evicted (and closed when no longer in use) before adding the new statement.

If a statement with the same query already exists, it will be replaced.

type TableOverrider

type TableOverrider interface {
	GetOverrideTable() string
}

TableOverrider interface allows relations to specify a custom table name.

type Tx

type Tx struct {
	Tx *sql.Tx
	// contains filtered or unexported fields
}

Tx wraps sql.Tx.

type ValidationError

type ValidationError struct {
	Field   string // Field name that failed validation
	Value   any    // The invalid value
	Message string // Human-readable error message
}

ValidationError represents a model validation failure

func (*ValidationError) Error

func (e *ValidationError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL