Skip to content

rezakhademix/zorm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

170 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Go Reference Go Report Card codecov License: MIT

ZORM

A Type-Safe, Production Ready Go ORM

One ORM To Query Them All


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

Key Features

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

Installation

go get github.com/rezakhademix/zorm

Quick Start

1. Connect to Database

PostgreSQL

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

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

zorm.GlobalDB = db

2. Define Models

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

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

Custom Table Name & Primary Key

// Custom table name
func (u User) TableName() string {
    return "app_users"
}

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

3. Basic CRUD

ctx := context.Background()

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

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

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

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

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

4. Bulk Operations

// CreateMany - Insert multiple records in a single query
users := []*User{
    {Name: "Alice", Email: "[email protected]"},
    {Name: "Bob", Email: "[email protected]"},
    {Name: "Charlie", Email: "[email protected]"},
}
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", "[email protected]").Exists(ctx)
if exists {
    fmt.Println("User exists!")
}

Pluck (Single Column)

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

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": "[email protected]"},  // Search attributes
    map[string]any{"name": "John", "age": 25},    // Values for creation
)

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

Pagination

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

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

Clone (Reuse Queries Safely)

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

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

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

Custom Table Name

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

Lifecycle Hooks

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

Available Hooks

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

Implementing Hooks

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

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

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

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

    return nil
}

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

    // updated_at is set automatically by ZORM

    return nil
}

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

Hook Execution Flow

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

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

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

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

Accessors (Computed Attributes)

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

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

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

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

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

Relationships

Defining Relations

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

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

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

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

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

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

Custom Table Names in Relations

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

Eager Loading

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

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

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

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

Lazy Loading

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

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

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

Many-to-Many Relations

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

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

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: "[email protected]"})
if err != nil {
    if qe := zorm.GetQueryError(err); qe != nil {
        fmt.Println(qe.Query)      // The SQL that failed
        fmt.Println(qe.Args)       // Query arguments
        fmt.Println(qe.Operation)  // "INSERT", "SELECT", etc.
        fmt.Println(qe.Table)      // Table name (if detected)
        fmt.Println(qe.Constraint) // Constraint name (if detected)
    }
}

Advanced Features

Statement Caching

Improve performance by reusing prepared statements:

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

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

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

Read/Write Splitting

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

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

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

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

Common Table Expressions (CTEs)

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

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

Full-Text Search (PostgreSQL)

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

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

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

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

Row Locking

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

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

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

Advanced Grouping

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

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

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

Chunking Large Datasets

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

Scopes (Reusable Query Logic)

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

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

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

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

Query Debugging

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

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

Complete Example

package main

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

    "github.com/rezakhademix/zorm"
)

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

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

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

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

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

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

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

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

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

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

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

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

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.

About

Z-ORM A Golang ORM to write fluent and fast queries. Query smarter, Code faster.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors