zorm

package module
v0.9.2 Latest Latest
Warning

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

Go to latest
Published: Feb 19, 2026 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: "john@example.com"}
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", "john@example.com").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)

4. Bulk Operations

// CreateMany - Insert multiple records in a single query
users := []*User{
    {Name: "Alice", Email: "alice@example.com"},
    {Name: "Bob", Email: "bob@example.com"},
    {Name: "Charlie", Email: "charlie@example.com"},
}
err := zorm.New[User]().CreateMany(ctx, users)
// All IDs are auto-populated after insert
fmt.Println(users[0].ID, users[1].ID, users[2].ID)

// UpdateMany - Update multiple records matching query
err = zorm.New[User]().
    Where("active", false).
    UpdateMany(ctx, map[string]any{"status": "inactive"})

// UpdateManyByKey - Update multiple records by matching lookup column to map keys
// Each map key is matched against the lookup column, and its value is set in the target column
updates := map[string]string{
    "REF001": "pending",
    "REF002": "approved",
    "REF003": "rejected",
}
err = zorm.New[Order]().UpdateManyByKey(ctx, "reference_number", "status", updates)

// DeleteMany - Delete multiple records matching query
err = zorm.New[User]().Where("status", "inactive").DeleteMany(ctx)

CreateMany Features:

  • Inserts all records in a single SQL statement for efficiency
  • Automatically chunks large batches to stay within database limits (65535 parameters for PostgreSQL)
  • Uses transactions for multi-chunk inserts to ensure atomicity
  • Returns inserted IDs via RETURNING clause
  • Works with all hooks (BeforeCreate is NOT called - use BulkInsert if you need hooks)
// For very large datasets, CreateMany automatically chunks
largeDataset := make([]*User, 10000)
for i := range largeDataset {
    largeDataset[i] = &User{Name: fmt.Sprintf("User %d", i)}
}
err := zorm.New[User]().CreateMany(ctx, largeDataset)
// Automatically split into multiple INSERT statements within a transaction

UpdateManyByKey - Efficient batch updates using CASE WHEN syntax:

// Example 1: Update order statuses by reference number
statusUpdates := map[string]string{
    "ORD-001": "shipped",
    "ORD-002": "delivered",
    "ORD-003": "cancelled",
}
err := zorm.New[Order]().UpdateManyByKey(ctx, "reference_number", "status", statusUpdates)
// Generates: UPDATE orders SET status = CASE reference_number
//            WHEN 'ORD-001' THEN 'shipped' WHEN 'ORD-002' THEN 'delivered' ... END
//            WHERE reference_number IN ('ORD-001', 'ORD-002', 'ORD-003')

// Example 2: Update product quantities by product code (int keys, int values)
quantityUpdates := map[int]int{
    100: 50,   // product code 100 -> quantity 50
    200: 75,   // product code 200 -> quantity 75
    300: 100,  // product code 300 -> quantity 100
}
err = zorm.New[Product]().UpdateManyByKey(ctx, "code", "quantity", quantityUpdates)

// Example 3: Combine with WHERE clause for conditional updates
// Only update orders that are in 'pending' status
statusUpdates := map[string]string{
    "ORD-001": "processing",
    "ORD-002": "processing",
}
err = zorm.New[Order]().
    Where("status", "pending").
    UpdateManyByKey(ctx, "reference_number", "status", statusUpdates)
// Only updates if both: reference_number matches AND status = 'pending'

UpdateManyByKey Features:

  • Uses efficient CASE WHEN syntax (single query for all updates)
  • Supports any map key/value types (string, int, float64, bool, etc.)
  • Automatically chunks large maps (500+ entries) with transaction safety
  • Combines with existing WHERE conditions
  • Auto-updates updated_at timestamp if the column exists

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
UpdateManyByKey(ctx, lookup, target, map) Update records by matching lookup column keys
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", "john@example.com").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)
}

Scalar Queries (Type-Safe Single Column)

ScalarQuery[T] provides a type-safe query builder for fetching single-column scalar values. Unlike Model[T] which returns full struct records, ScalarQuery returns simple typed values like []string, []int64, []float64, etc.

// Example 1: Get all usernames from users table
names, err := zorm.Query[string]().
    Table("users").
    Select("name").
    Where("active", true).
    Get(ctx)
// names is []string{"Alice", "Bob", "Charlie"}

// Example 2: Get user IDs ordered by creation date
ids, err := zorm.Query[int64]().
    Table("users").
    Select("id").
    OrderBy("created_at", "DESC").
    Limit(100).
    Get(ctx)
// ids is []int64{42, 41, 40, ...}

// Example 3: Get distinct roles with count filtering
roles, err := zorm.Query[string]().
    Table("users").
    Select("role").
    Distinct().
    GroupBy("role").
    Having("COUNT(*) >", 5).
    Get(ctx)
// roles is []string{"admin", "editor"} (roles with more than 5 users)

ScalarQuery supports the same query builder methods as Model:

  • Where, OrWhere, WhereIn, WhereNull, WhereNotNull
  • OrderBy, Limit, Offset
  • Distinct, GroupBy, Having
  • First (returns single value), Count (returns row count)
  • SetDB, WithTx, Clone, Print

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": "john@example.com"},  // 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": "john@example.com"},  // 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: "JOHN@EXAMPLE.COM"}
err := zorm.New[User]().Create(ctx, user)
// BeforeCreate lowercases email to "john@example.com"

// 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
    }
}
Managing Many-to-Many Associations

ZORM provides three methods to manage pivot table associations: Attach, Detach, and Sync.

user := &User{ID: 1}

// Attach - Add new associations (inserts into pivot table)
err := zorm.New[User]().Attach(ctx, user, "Roles", []any{3, 4}, nil)
// Adds role_user entries: (1,3), (1,4)

// Attach with pivot data (extra columns in pivot table)
pivotData := map[any]map[string]any{
    3: {"assigned_at": time.Now(), "assigned_by": 1},
    4: {"assigned_at": time.Now(), "assigned_by": 1},
}
err = zorm.New[User]().Attach(ctx, user, "Roles", []any{3, 4}, pivotData)

// Detach - Remove specific associations
err = zorm.New[User]().Detach(ctx, user, "Roles", []any{2})
// Removes role_user entry: (1,2)

// Detach all - Remove all associations for the relation
err = zorm.New[User]().Detach(ctx, user, "Roles", nil)
// Removes all role_user entries where user_id = 1
Sync - Synchronize Associations

Sync is a handy method for managing many-to-many relations. It synchronizes the pivot table to match exactly the IDs you provide:

  • Attaches IDs that are in the new list but not in the database
  • Detaches IDs that are in the database but not in the new list
  • Keeps IDs that exist in both (no duplicate entry errors)
user := &User{ID: 1}
// Current roles in DB: [1, 2, 3]

// Sync to new set of roles
err := zorm.New[User]().Sync(ctx, user, "Roles", []any{1, 2, 4}, nil)
// Result:
// - Role 1: kept (exists in both)
// - Role 2: kept (exists in both)
// - Role 3: detached (was in DB, not in new list)
// - Role 4: attached (not in DB, is in new list)
// Final roles in DB: [1, 2, 4]

// Sync with pivot data for new attachments
pivotData := map[any]map[string]any{
    4: {"assigned_at": time.Now()},
}
err = zorm.New[User]().Sync(ctx, user, "Roles", []any{1, 2, 4}, pivotData)

Common Sync Use Cases:

// Replace all user roles with a new set
err := zorm.New[User]().Sync(ctx, user, "Roles", []any{1, 2}, nil)

// Remove all roles (sync with empty list)
err = zorm.New[User]().Sync(ctx, user, "Roles", []any{}, nil)

// Form submission: update user roles from checkbox selection
selectedRoleIDs := []any{1, 3, 5}  // From form
err = zorm.New[User]().Sync(ctx, user, "Roles", selectedRoleIDs, nil)

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: "duplicate@example.com"})
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: "john@example.com", 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": "jane@example.com"},
        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)
}

AI-Assisted Development

This project was developed with the help of AI tools, using Claude Code. While AI contributed to code suggestions and ideas, all AI-generated code was reviewed by humans, and nothing was automatically approved.

This repository is not entirely AI-written or vibe coded; it reflects modern programming practices enhanced by AI assistance. AI was used as a tool to accelerate development, not replace human judgment and you can see Claude Code as a contributor

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")
	// ErrNilDatabase is returned when database connection is nil
	ErrNilDatabase = errors.New("zorm: database connection is nil")

	// 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")
)
View Source
var ErrInvalidColumnName = fmt.Errorf("zorm: invalid column name")

ErrInvalidColumnName is returned when a column name contains invalid characters.

View Source
var ErrRollbackFailed = errors.New("zorm: rollback failed")

ErrRollbackFailed is returned when transaction rollback fails

View Source
var GlobalDB *sql.DB

GlobalDB is the global database connection pool. For thread-safe access in concurrent code, prefer SetGlobalDB() for writes. Reads via GetGlobalDB() will check both this variable and the atomic pointer for backwards compatibility with code that directly assigns to GlobalDB.

Functions

func ClearAllOriginals added in v0.8.0

func ClearAllOriginals()

ClearAllOriginals removes all tracking data from the global tracker. This can be used for periodic cleanup in long-running services.

func ClearDBResolver added in v0.8.0

func ClearDBResolver()

ClearDBResolver removes the global database resolver. This function is thread-safe.

func ClearOriginals added in v0.7.3

func ClearOriginals[T any](entity *T)

ClearOriginals removes tracking for an entity. Should be called when entity is deleted or no longer needed to prevent memory leaks.

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. This function is thread-safe and can be called at any time. Example:

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

func ConfigureDirtyTracking added in v0.8.0

func ConfigureDirtyTracking(maxEntities int)

ConfigureDirtyTracking sets the maximum number of entities to track. A capacity of 0 means unbounded (no eviction). The default capacity is 50,000 entities. This function is thread-safe and can be called at any time.

Example:

zorm.ConfigureDirtyTracking(10000) // Track at most 10,000 entities

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 GetGlobalDB added in v0.8.0

func GetGlobalDB() *sql.DB

GetGlobalDB returns the global database connection in a thread-safe manner. For backwards compatibility, it checks both the atomic pointer and the deprecated GlobalDB variable, preferring the atomic if set.

func GetOriginal added in v0.7.3

func GetOriginal[T any](entity *T, column string) any

GetOriginal returns the original value of a field before any modifications. Returns nil if the entity is not tracked or field doesn't exist.

func GetOriginals added in v0.7.3

func GetOriginals[T any](entity *T) map[string]any

GetOriginals returns all original values for the entity. Returns a copy to prevent external modification of tracking data.

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 IsTracked added in v0.7.3

func IsTracked[T any](entity *T) bool

IsTracked returns true if the entity has original values stored. An entity becomes tracked when loaded from the database via Get, First, or Find.

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 SetGlobalDB added in v0.8.0

func SetGlobalDB(db *sql.DB)

SetGlobalDB sets the global database connection in a thread-safe manner. This also updates the GlobalDB variable for backwards compatibility.

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 TrackedEntityCount added in v0.8.0

func TrackedEntityCount() int

TrackedEntityCount returns the number of currently tracked entities. Useful for monitoring memory usage in long-running services.

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. Results are cached to avoid repeated validation of the same column names.

func ValidateRawQuery added in v0.7.0

func ValidateRawQuery(query string) error

ValidateRawQuery validates a raw SQL query fragment to prevent SQL injection. It checks for dangerous patterns like comments, multiple statements, and suspicious keywords. This is used for HAVING clauses and other places where raw query fragments are accepted.

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. For PostgreSQL, it uses SQLSTATE codes for reliable error detection; for other databases, it falls back to error message pattern matching.

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. Automatically tracks original values for dirty checking. If a tracking scope is configured, entities are registered with the scope.

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.

func GetGlobalResolver added in v0.8.0

func GetGlobalResolver() *DBResolver

GetGlobalResolver returns the current global database resolver. Returns nil if no resolver is configured.

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       // 16 bytes
	Column    string       // 16 bytes
	FieldType reflect.Type // 16 bytes
	Index     []int        // 24 bytes
	IsPrimary bool         // 1 byte
	IsAuto    bool         // 1 byte + 6 padding
}

FieldInfo holds data about a single field in the model. Struct layout is optimized to minimize padding on 64-bit systems.

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.

Thread Safety: Model instances are NOT safe for concurrent modification. Query builder methods (Where, Select, OrderBy, etc.) mutate internal state without locking and must not be called concurrently on the same Model instance.

Safe patterns for concurrent use:

  1. Build a base model in a single goroutine, then clone it for concurrent handlers. The base model must not be modified after the setup phase. Clone() on a read-only base is safe from multiple goroutines.
  2. Create a fresh Model via New[T]() inside each goroutine.

Example:

base := New[User]().Where("active", true) // setup phase — single goroutine
// SAFE: base is never written again; clones get independent state
go func() { base.Clone().Where("role", "admin").Get(ctx) }()
go func() { base.Clone().Where("role", "user").Get(ctx) }()

// UNSAFE: Concurrent mutation of same Model — DATA RACE
go func() { base.Where("role", "admin").Get(ctx) }()
go func() { base.Where("role", "user").Get(ctx) }()

func Acquire added in v0.7.1

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

Acquire retrieves a Model[T] from the pool for high-throughput scenarios. The returned model is pre-configured with default values and ready for use. Call Release() when done to return the model to the pool.

Example:

m := Acquire[User]()
defer m.Release()
users, err := m.Where("active", true).Get(ctx)

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. Column names are validated to prevent SQL injection. This method is safe for concurrent use - it clones the model before modification.

func (*Model[T]) BulkInsert added in v0.7.1

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

BulkInsert inserts multiple records using a single prepared statement. This is more efficient than CreateMany for scenarios where you need fine-grained control or want to handle errors per-entity. The prepared statement is reused for each entity, reducing preparation overhead.

Example:

users := []*User{{Name: "Alice"}, {Name: "Bob"}, {Name: "Charlie"}}
err := model.BulkInsert(ctx, users)

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. Uses Clone() for each iteration to avoid mutating the original query state.

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.

Thread Safety: Model instances are NOT safe for concurrent modification. Clone() itself is safe to call from multiple goroutines on a model that is no longer being modified (e.g., a base model built in a setup phase). It is NOT safe to call Clone() concurrently with any mutating method (Where, Select, OrderBy, etc.) on the same instance — those methods do not acquire any lock.

Safe patterns:

// Build base in a single goroutine, then clone concurrently (no further writes to base)
base := New[User]().Where("active", true) // single-goroutine setup
go func() { base.Clone().Where("role", "admin").Get(ctx) }()
go func() { base.Clone().Where("role", "user").Get(ctx) }()

// Or create a fresh model per goroutine
go func() { New[User]().Where("active", true).Get(ctx) }()

func (*Model[T]) Count

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

Count returns the number of records matching the query. This method is safe for concurrent use - it clones the model before modification. When the query includes GROUP BY, DISTINCT, or DISTINCT ON, the count is wrapped in a subquery to return the correct total number of rows.

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. Column names are validated to prevent SQL injection. This method is safe for concurrent use - it clones the model before modification.

func (*Model[T]) Create

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

Create inserts a new record.

Hook Behavior: If the entity implements BeforeCreate(context.Context) error, it will be called before the INSERT. If BeforeCreate succeeds but INSERT fails, any side effects from BeforeCreate are NOT rolled back automatically.

For atomic operations with hooks that have side effects, wrap in a transaction:

err := zorm.Transaction(ctx, func(tx *zorm.Tx) error {
    return model.WithTx(tx).Create(ctx, entity)
})

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]) CrossJoin added in v0.9.2

func (m *Model[T]) CrossJoin(table string) *Model[T]

CrossJoin adds a CROSS JOIN clause, producing a Cartesian product of both tables. No ON condition is required. Table name is validated to prevent SQL injection.

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.

Relation loading (With, WithCallback, WithMorph) is not supported with Cursor because it requires all rows to be available for batch loading. Use Get() instead when relation loading is needed.

func (*Model[T]) Delete

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

Delete deletes records matching the current query conditions. At least one WHERE condition is required to prevent accidental full-table deletes. To intentionally delete all records use ForceDeleteAll().

func (*Model[T]) DeleteMany

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

DeleteMany deletes records matching the current query conditions. Alias for Delete(). At least one WHERE condition is required. To intentionally delete all records use ForceDeleteAll().

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. Column names are validated to prevent SQL injection.

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. This method is safe for concurrent use - it clones the model before modification.

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. Uses Clone() to avoid mutating the original query state.

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]) ForceDeleteAll added in v0.9.2

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

ForceDeleteAll deletes ALL records in the table without any WHERE conditions. Use this only when a full-table delete is intentional.

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]) GetDirtyFields added in v0.7.3

func (m *Model[T]) GetDirtyFields(entity *T) map[string]any

GetDirtyFields returns all changed fields on an entity. This is a convenience method on Model.

func (*Model[T]) GetLimit added in v0.9.1

func (m *Model[T]) GetLimit() int

GetLimit returns the limit value.

func (*Model[T]) GetOrderBys added in v0.9.1

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

GetOrderBys returns the order by clauses.

func (*Model[T]) GetOriginalValue added in v0.7.3

func (m *Model[T]) GetOriginalValue(entity *T, column string) any

GetOriginalValue returns the original value of a field. This is a convenience method on Model.

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. Column names are validated to prevent SQL injection.

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 (). Column names are validated to prevent SQL injection.

func (*Model[T]) GroupByRollup

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

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

func (*Model[T]) HasDirtyFields added in v0.8.0

func (m *Model[T]) HasDirtyFields(entity *T) bool

HasDirtyFields returns true if the entity has any dirty (changed) fields. This is more efficient than GetDirtyFields when you only need to check existence.

func (*Model[T]) Having

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

Having adds a HAVING clause (used with GROUP BY). The query string is validated to prevent SQL injection by checking for dangerous patterns. Use parameterized values (?) for user input.

SECURITY WARNING: While ValidateRawQuery prevents traditional SQL injection attacks, it cannot prevent logical injection (e.g., "COUNT(*) > ? OR 1=1"). The query parameter should be a trusted constant expression. DO NOT pass unsanitized user input directly.

Safe example:

Having("COUNT(*) > ?", 5)
Having("SUM(amount) >= ?", 1000)

Example: Having("COUNT(*) > ?", 5)

func (*Model[T]) IsCleanField added in v0.7.3

func (m *Model[T]) IsCleanField(entity *T, column string) bool

IsCleanField checks if a specific field on an entity is clean (unchanged). This is a convenience method on Model.

func (*Model[T]) IsDirtyField added in v0.7.3

func (m *Model[T]) IsDirtyField(entity *T, column string) bool

IsDirtyField checks if a specific field on an entity is dirty. This is a convenience method on Model.

func (*Model[T]) IsEntityTracked added in v0.7.3

func (m *Model[T]) IsEntityTracked(entity *T) bool

IsEntityTracked returns true if the entity is being tracked for dirty checking. This is a convenience method on Model.

func (*Model[T]) Join added in v0.9.2

func (m *Model[T]) Join(table, col1, op, col2 string) *Model[T]

Join adds an INNER JOIN clause. Both table and column names are validated to prevent SQL injection. Returns the model with buildErr set on invalid input; the error is surfaced when a terminal method (Get, First, etc.) is called.

Example:

New[Order]().Join("users", "orders.user_id", "=", "users.id").Get(ctx)

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]) LeftJoin added in v0.9.2

func (m *Model[T]) LeftJoin(table, col1, op, col2 string) *Model[T]

LeftJoin adds a LEFT JOIN clause. Returns all rows from the left table and matching rows from the right table. Column names are validated to prevent SQL injection.

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. This method creates an internal clone to avoid mutating the original model's state, making it safe to reuse the model for subsequent queries.

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. This method creates an internal clone to avoid mutating the original model's state, making it safe to reuse the model for subsequent queries.

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. This method creates an internal clone to avoid mutating the original model's state, making it safe to reuse the model for subsequent queries.

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] Lock modes are validated against a whitelist to prevent SQL injection.

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]) Omit added in v0.7.3

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

Omit excludes columns from Update operations. This is useful when you want to update most fields but exclude specific ones.

Example:

err := model.Omit("phone_number", "address").Update(ctx, user)
// Updates all fields EXCEPT phone_number and address

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. Column names are validated to prevent SQL injection.

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. Column names are validated to prevent SQL injection.

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. Uses Clone() to avoid mutating the original query state. 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. Column names are validated to prevent SQL injection. This method is safe for concurrent use - it clones the model before modification.

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]) Release added in v0.7.1

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

Release returns the Model to the pool for reuse. After calling Release, the Model should not be used again.

func (*Model[T]) RightJoin added in v0.9.2

func (m *Model[T]) RightJoin(table, col1, op, col2 string) *Model[T]

RightJoin adds a RIGHT JOIN clause. Returns all rows from the right table and matching rows from the left table. Column names are validated to prevent SQL injection.

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. Returns the model with buildErr set if any column name is invalid; the error is surfaced when a terminal method (Get, First, etc.) is called.

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. Uses Clone() to avoid mutating the original query state. 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. Column names are validated to prevent SQL injection. This method is safe for concurrent use - it clones the model before modification.

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.

Hook Behavior:

  • BeforeUpdate: Called before UPDATE. If it succeeds but UPDATE fails, side effects are NOT rolled back.
  • AfterUpdate: Called after successful UPDATE. If it fails, the UPDATE is already committed and will NOT be rolled back.

For atomic operations with hooks that have side effects, wrap in a transaction:

err := zorm.Transaction(ctx, func(tx *zorm.Tx) error {
    return model.WithTx(tx).Update(ctx, entity)
})

func (*Model[T]) UpdateColumns added in v0.7.3

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

UpdateColumns updates only the specified columns of the entity. This is useful when you want explicit control over which columns are updated.

Example:

user.Name = "New Name"
user.Email = "new@email.com"
err := model.UpdateColumns(ctx, user, "name", "email")  // Only updates name and email

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]) UpdateManyByKey added in v0.8.3

func (m *Model[T]) UpdateManyByKey(ctx context.Context, lookupColumn, targetColumn string, updates any) error

UpdateManyByKey updates multiple records by matching a lookup column to values in a map. Each map key is matched against lookupColumn, and the corresponding map value is set in targetColumn. Uses CASE WHEN syntax for database portability.

Example:

updates := map[string]string{"REF001": "pending", "REF002": "approved"}
err := New[Text]().UpdateManyByKey(ctx, "reference_number", "status", updates)

This generates SQL like:

UPDATE texts SET status = CASE reference_number
    WHEN $1 THEN $2
    WHEN $3 THEN $4
END, updated_at = $5
WHERE reference_number IN ($6, $7)

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. Column names are validated to prevent SQL injection. 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. Column names and config are validated to prevent SQL injection. 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. Column names are validated to prevent SQL injection.

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. Column names are validated to prevent SQL injection.

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. Column names are validated to prevent SQL injection. 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. Column names are validated to prevent SQL injection. 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]. CTE names are validated to prevent SQL injection.

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]) WithTrackingScope added in v0.8.0

func (m *Model[T]) WithTrackingScope(scope *TrackingScope) *Model[T]

WithTrackingScope sets a tracking scope for this model instance. All entities loaded through this model will be registered with the scope, and their tracking data will be automatically cleared when the scope is closed.

This is useful for batch operations where you want automatic cleanup of tracking data without memory leaks.

Example:

scope := zorm.NewTrackingScope()
defer scope.Close()
model := zorm.New[User]().WithTrackingScope(scope)
users, _ := model.Get(ctx) // All users are tracked in scope
// When scope.Close() is called, all tracking data is cleared

func (*Model[T]) WithTx

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

WithTx returns a clone of the model with the transaction set. This ensures the original model is not mutated, allowing safe reuse.

Example:

base := New[User]().Where("active", true)
Transaction(ctx, func(tx *Tx) error {
    // base is not mutated; txModel is a separate copy
    txModel := base.WithTx(tx)
    return txModel.Create(ctx, &user)
})
// base can still be used outside the transaction

type ModelInfo

type ModelInfo struct {
	Type            reflect.Type
	TableName       string
	PrimaryKey      string
	Fields          map[string]*FieldInfo // StructFieldName -> FieldInfo
	Columns         map[string]*FieldInfo // DBColumnName -> FieldInfo
	RelationFields  map[string][]int      // FieldName -> field index for FieldByIndex (relation fields)
	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. Uses sync.Map for thread-safe caching with optimal read performance.

func (*ModelInfo) GetRelationField added in v0.8.0

func (m *ModelInfo) GetRelationField(structVal reflect.Value, fieldName string) reflect.Value

GetRelationField returns the reflect.Value for a relation field by name. Uses cached field indices for O(1) access instead of O(n) FieldByName. Returns invalid Value if field not found.

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.

Unlike other relation types, MorphTo cannot implement NewRelated() and NewModel() in a meaningful way because the related type is determined dynamically at runtime based on the Type column value. These methods intentionally return nil. The TypeMap field maps database type strings to empty struct instances, which are used during eager loading to determine and instantiate the correct related type.

Example:

func (i *Image) ImageableRelation() MorphTo[any] {
    return MorphTo[any]{
        Type: "imageable_type",
        ID:   "imageable_id",
        TypeMap: map[string]any{
            "posts": Post{},
            "users": User{},
        },
    }
}

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

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

NewModel returns nil for MorphTo because the related model type is determined dynamically at runtime based on the type column value.

func (MorphTo[T]) NewRelated

func (MorphTo[T]) NewRelated() any

NewRelated returns nil for MorphTo because the related type is determined dynamically at runtime based on the type column value. Use TypeMap instead.

func (MorphTo[T]) RelationType

func (MorphTo[T]) RelationType() RelationType

MorphTo implements Relation interface. RelationType returns RelationMorphTo.

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 selects a replica randomly for load distribution. This provides non-deterministic load balancing which can help prevent hotspots when multiple clients start at the same time.

func (*RandomLoadBalancer) Next

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

Next returns a randomly selected replica from the pool.

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 ScalarQuery added in v0.8.2

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

ScalarQuery provides a query builder for single-column scalar results. T can be any type that sql.Rows.Scan supports: string, int, int64, float64, bool, time.Time, []byte, sql.Null* types, or any sql.Scanner.

Example:

names, err := zorm.Query[string]().
    Table("roles").
    Select("name").
    Where("active", true).
    Get(ctx)

func Query added in v0.8.2

func Query[T any]() *ScalarQuery[T]

Query creates a new scalar query builder for type T.

func (*ScalarQuery[T]) Clone added in v0.8.2

func (q *ScalarQuery[T]) Clone() *ScalarQuery[T]

Clone creates a deep copy of the query. This is useful for creating variations of a query without mutating the original.

func (*ScalarQuery[T]) Count added in v0.8.2

func (q *ScalarQuery[T]) Count(ctx context.Context) (int64, error)

Count returns the count of matching rows. This ignores the Select column and uses COUNT(*).

func (*ScalarQuery[T]) Distinct added in v0.8.2

func (q *ScalarQuery[T]) Distinct() *ScalarQuery[T]

Distinct adds DISTINCT to the query.

func (*ScalarQuery[T]) First added in v0.8.2

func (q *ScalarQuery[T]) First(ctx context.Context) (T, error)

First returns the first matching value. Returns ErrRecordNotFound if no rows match. Uses Clone() to avoid mutating the original query's limit.

func (*ScalarQuery[T]) Get added in v0.8.2

func (q *ScalarQuery[T]) Get(ctx context.Context) ([]T, error)

Get executes the query and returns all matching values.

func (*ScalarQuery[T]) GroupBy added in v0.8.2

func (q *ScalarQuery[T]) GroupBy(columns ...string) *ScalarQuery[T]

GroupBy adds GROUP BY columns.

func (*ScalarQuery[T]) Having added in v0.8.2

func (q *ScalarQuery[T]) Having(query string, args ...any) *ScalarQuery[T]

Having adds a HAVING clause.

func (*ScalarQuery[T]) Limit added in v0.8.2

func (q *ScalarQuery[T]) Limit(n int) *ScalarQuery[T]

Limit sets the LIMIT clause.

func (*ScalarQuery[T]) Offset added in v0.8.2

func (q *ScalarQuery[T]) Offset(n int) *ScalarQuery[T]

Offset sets the OFFSET clause.

func (*ScalarQuery[T]) OrWhere added in v0.8.2

func (q *ScalarQuery[T]) OrWhere(query any, args ...any) *ScalarQuery[T]

OrWhere adds an OR WHERE condition.

func (*ScalarQuery[T]) OrderBy added in v0.8.2

func (q *ScalarQuery[T]) OrderBy(column, direction string) *ScalarQuery[T]

OrderBy adds an ORDER BY clause.

func (*ScalarQuery[T]) Print added in v0.8.2

func (q *ScalarQuery[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.

func (*ScalarQuery[T]) Select added in v0.8.2

func (q *ScalarQuery[T]) Select(column string) *ScalarQuery[T]

Select sets the column to select (single column only).

func (*ScalarQuery[T]) SetDB added in v0.8.2

func (q *ScalarQuery[T]) SetDB(db *sql.DB) *ScalarQuery[T]

SetDB sets a specific database connection.

func (*ScalarQuery[T]) Table added in v0.8.2

func (q *ScalarQuery[T]) Table(name string) *ScalarQuery[T]

Table sets the table name for the query. Table names are validated to prevent SQL injection.

func (*ScalarQuery[T]) Where added in v0.8.2

func (q *ScalarQuery[T]) Where(query any, args ...any) *ScalarQuery[T]

Where adds a WHERE condition. Supports multiple forms:

Where("column", value) -> column = ?
Where("column >", value) -> column > ?
Where("column", ">", value) -> column > ?
Where(map[string]any{"name": "John"}) -> name = ?

func (*ScalarQuery[T]) WhereIn added in v0.8.2

func (q *ScalarQuery[T]) WhereIn(column string, values []any) *ScalarQuery[T]

WhereIn adds a WHERE column IN (...) condition.

func (*ScalarQuery[T]) WhereNotNull added in v0.8.2

func (q *ScalarQuery[T]) WhereNotNull(column string) *ScalarQuery[T]

WhereNotNull adds a WHERE column IS NOT NULL condition.

func (*ScalarQuery[T]) WhereNull added in v0.8.2

func (q *ScalarQuery[T]) WhereNull(column string) *ScalarQuery[T]

WhereNull adds a WHERE column IS NULL condition.

func (*ScalarQuery[T]) WithTx added in v0.8.2

func (q *ScalarQuery[T]) WithTx(tx *Tx) *ScalarQuery[T]

WithTx uses a transaction for the query.

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 uses sharded locking to reduce contention under high concurrency. It 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. In-flight statements (with refCount > 0) will be closed when their release function is called after the last user is done.

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.

func (*StmtCache) PutAndGet added in v0.7.0

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

PutAndGet atomically stores a prepared statement and retrieves it with an incremented reference count. This avoids race conditions where the statement could be evicted between Put and Get calls. Returns the statement and a release function. The caller MUST call the release function.

type TableOverrider

type TableOverrider interface {
	GetOverrideTable() string
}

TableOverrider interface allows relations to specify a custom table name.

type TrackingScope added in v0.8.0

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

TrackingScope provides scoped dirty tracking for batch operations. Entities tracked within a scope are automatically cleaned up when the scope is closed. This is useful for processing large batches of entities where you don't want tracking data to persist beyond the operation.

func NewTrackingScope added in v0.8.0

func NewTrackingScope() *TrackingScope

NewTrackingScope creates a new tracking scope.

func (*TrackingScope) Close added in v0.8.0

func (s *TrackingScope) Close()

Close clears all tracking data for entities in this scope. After Close is called, the scope should not be reused.

func (*TrackingScope) Len added in v0.8.0

func (s *TrackingScope) Len() int

Len returns the number of entities tracked in this scope.

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