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.
- Type-Safe: Full compile-time type safety powered by Go generics
- Zero Dependencies: Built on Go's
database/sqlpackage, 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.Contextfor cancellation & timeout - Debugging:
Print()method to inspect generated SQL without executing - Lifecycle Hooks: BeforeCreate, BeforeUpdate, AfterUpdate hooks
- Accessors: Computed attributes via getter methods
go get github.com/rezakhademix/zormimport (
"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 = dbModels 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
func (u User) TableName() string {
return "app_users"
}
// Custom primary key
func (u User) PrimaryKey() string {
return "user_id"
}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)// 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
RETURNINGclause - Works with all hooks (
BeforeCreateis NOT called - useBulkInsertif 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 transactionUpdateManyByKey - 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_attimestamp if the column exists
| 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 |
| 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 |
| 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 |
| 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 |
// 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)// 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!")
}// 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)
}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,WhereNotNullOrderBy,Limit,OffsetDistinct,GroupBy,HavingFirst(returns single value),Count(returns row count)SetDB,WithTx,Clone,Print
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)
}// 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
)// 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)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)// Override table name for this query
users, _ := zorm.New[User]().Table("archived_users").Get(ctx)ZORM supports lifecycle hooks that are automatically called during CRUD operations.
| Hook | When Called |
|---|---|
BeforeCreate(ctx) |
Before INSERT |
BeforeUpdate(ctx) |
Before UPDATE |
AfterUpdate(ctx) |
After UPDATE |
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
}// 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)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"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
}
}func (u User) PostsRelation() zorm.HasMany[Post] {
return zorm.HasMany[Post]{
ForeignKey: "user_id",
Table: "blog_posts", // Use custom table name
}
}// 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)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")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
}
}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 = 1Sync 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)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)// 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
ZORM provides comprehensive error handling with categorized 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 erroruser, 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
}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)
}
}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// 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)// 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)// 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)// 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)// 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)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
})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)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]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)
}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
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.