{"id":34847852,"url":"https://github.com/rezakhademix/zorm","last_synced_at":"2026-02-11T07:16:32.914Z","repository":{"id":312990755,"uuid":"1049283537","full_name":"rezakhademix/zorm","owner":"rezakhademix","description":"Z-ORM A Golang ORM to write fluent and fast queries. Query smarter, Code faster. ","archived":false,"fork":false,"pushed_at":"2026-01-28T09:46:23.000Z","size":383,"stargazers_count":3,"open_issues_count":0,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-29T01:19:16.257Z","etag":null,"topics":["go","golang","orm","web"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/rezakhademix/zorm","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rezakhademix.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-02T18:45:18.000Z","updated_at":"2026-01-28T09:45:28.000Z","dependencies_parsed_at":"2025-09-03T09:21:38.145Z","dependency_job_id":"e01f8c11-d3b0-45c7-a9bf-bf422f7a9f85","html_url":"https://github.com/rezakhademix/zorm","commit_stats":null,"previous_names":["rezakhademix/zorm"],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/rezakhademix/zorm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezakhademix%2Fzorm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezakhademix%2Fzorm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezakhademix%2Fzorm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezakhademix%2Fzorm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rezakhademix","download_url":"https://codeload.github.com/rezakhademix/zorm/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rezakhademix%2Fzorm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29116450,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-05T05:31:32.482Z","status":"ssl_error","status_checked_at":"2026-02-05T05:31:29.075Z","response_time":65,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","orm","web"],"created_at":"2025-12-25T18:54:18.201Z","updated_at":"2026-02-05T08:01:16.180Z","avatar_url":"https://github.com/rezakhademix.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Go Reference](https://pkg.go.dev/badge/github.com/rezakhademix/zorm.svg)](https://pkg.go.dev/github.com/rezakhademix/zorm) [![Go Report Card](https://goreportcard.com/badge/github.com/rezakhademix/zorm)](https://goreportcard.com/report/github.com/rezakhademix/zorm) [![codecov](https://codecov.io/gh/rezakhademix/zorm/graph/badge.svg?token=BDWNVIC670)](https://codecov.io/gh/rezakhademix/zorm) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n\n\u003cdiv align=\"center\"\u003e\n  \u003ch1\u003eZORM\u003c/h1\u003e\n  \u003cp\u003e\u003cstrong\u003eA Type-Safe, Production Ready Go ORM\u003c/strong\u003e\u003c/p\u003e\n  \u003cp\u003eOne ORM To Query Them All\u003c/p\u003e\n\u003c/div\u003e\n\n---\n\nZORM 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.\n\n## Key Features\n\n- **Type-Safe**: Full compile-time type safety powered by Go generics\n- **Zero Dependencies**: Built on Go's `database/sql` package, works with any SQL driver\n- **High Performance**: Prepared statement caching and connection pooling\n- **Relations**: HasOne, HasMany, BelongsTo, BelongsToMany, Polymorphic relations\n- **Fluent API**: Chainable query builder with intuitive method names\n- **Advanced Queries**: CTEs, Subqueries, Full-Text Search, Window Functions\n- **Database Splitting**: Automatic read/write split with replica support\n- **Context Support**: All operations respect `context.Context` for cancellation \u0026 timeout\n- **Debugging**: `Print()` method to inspect generated SQL without executing\n- **Lifecycle Hooks**: BeforeCreate, BeforeUpdate, AfterUpdate hooks\n- **Accessors**: Computed attributes via getter methods\n\n  \n## AI-Assisted Development\n\nThis 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.\n\nThis 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\n\n\n## Installation\n\n```bash\ngo get github.com/rezakhademix/zorm\n```\n\n## Quick Start\n\n### 1. Connect to Database\n\n#### PostgreSQL\n\n```go\nimport (\n    \"github.com/rezakhademix/zorm\"\n)\n\n// Using helper (with connection pooling)\ndb, err := zorm.ConnectPostgres(\n    \"postgres://user:password@localhost/dbname?sslmode=disable\",\n    \u0026zorm.DBConfig{\n        MaxOpenConns:    25,\n        MaxIdleConns:    5,\n        ConnMaxLifetime: time.Hour,\n        ConnMaxIdleTime: 30 * time.Minute,\n    },\n)\n\nzorm.GlobalDB = db\n```\n\n### 2. Define Models\n\nModels are standard Go structs. **ZORM uses convention over configuration** - no tags required!\n\n```go\ntype User struct {\n    ID        int64      // Automatically detected as primary key with auto-increment\n    Name      string     // Maps to \"name\" column\n    Email     string     // Maps to \"email\" column\n    Age       int        // Maps to \"age\" column\n    CreatedAt time.Time  // Maps to \"created_at\" column\n    UpdatedAt time.Time  // Maps to \"updated_at\" (auto-updated)\n}\n// Table name: \"users\" (auto-pluralized snake_case)\n```\n\n#### Custom Table Name \u0026 Primary Key\n\n```go\n// Custom table name\nfunc (u User) TableName() string {\n    return \"app_users\"\n}\n\n// Custom primary key\nfunc (u User) PrimaryKey() string {\n    return \"user_id\"\n}\n```\n\n### 3. Basic CRUD\n\n```go\nctx := context.Background()\n\n// Create\nuser := \u0026User{Name: \"John\", Email: \"john@example.com\"}\nerr := zorm.New[User]().Create(ctx, user)\nfmt.Println(user.ID) // Auto-populated after insert\n\n// Read - Single\nuser, err := zorm.New[User]().Find(ctx, 1)\nuser, err := zorm.New[User]().Where(\"email\", \"john@example.com\").First(ctx)\n\n// Read - Multiple\nusers, err := zorm.New[User]().Where(\"age\", \"\u003e\", 18).Get(ctx)\n\n// Update\nuser.Name = \"Jane\"\nerr = zorm.New[User]().Update(ctx, user) // updated_at auto-set\n\n// Delete\nerr = zorm.New[User]().Where(\"id\", 1).Delete(ctx)\n```\n\n### 4. Bulk Operations\n\n```go\n// CreateMany - Insert multiple records in a single query\nusers := []*User{\n    {Name: \"Alice\", Email: \"alice@example.com\"},\n    {Name: \"Bob\", Email: \"bob@example.com\"},\n    {Name: \"Charlie\", Email: \"charlie@example.com\"},\n}\nerr := zorm.New[User]().CreateMany(ctx, users)\n// All IDs are auto-populated after insert\nfmt.Println(users[0].ID, users[1].ID, users[2].ID)\n\n// UpdateMany - Update multiple records matching query\nerr = zorm.New[User]().\n    Where(\"active\", false).\n    UpdateMany(ctx, map[string]any{\"status\": \"inactive\"})\n\n// UpdateManyByKey - Update multiple records by matching lookup column to map keys\n// Each map key is matched against the lookup column, and its value is set in the target column\nupdates := map[string]string{\n    \"REF001\": \"pending\",\n    \"REF002\": \"approved\",\n    \"REF003\": \"rejected\",\n}\nerr = zorm.New[Order]().UpdateManyByKey(ctx, \"reference_number\", \"status\", updates)\n\n// DeleteMany - Delete multiple records matching query\nerr = zorm.New[User]().Where(\"status\", \"inactive\").DeleteMany(ctx)\n```\n\n**CreateMany Features:**\n- Inserts all records in a single SQL statement for efficiency\n- Automatically chunks large batches to stay within database limits (65535 parameters for PostgreSQL)\n- Uses transactions for multi-chunk inserts to ensure atomicity\n- Returns inserted IDs via `RETURNING` clause\n- Works with all hooks (`BeforeCreate` is NOT called - use `BulkInsert` if you need hooks)\n\n```go\n// For very large datasets, CreateMany automatically chunks\nlargeDataset := make([]*User, 10000)\nfor i := range largeDataset {\n    largeDataset[i] = \u0026User{Name: fmt.Sprintf(\"User %d\", i)}\n}\nerr := zorm.New[User]().CreateMany(ctx, largeDataset)\n// Automatically split into multiple INSERT statements within a transaction\n```\n\n**UpdateManyByKey** - Efficient batch updates using CASE WHEN syntax:\n\n```go\n// Example 1: Update order statuses by reference number\nstatusUpdates := map[string]string{\n    \"ORD-001\": \"shipped\",\n    \"ORD-002\": \"delivered\",\n    \"ORD-003\": \"cancelled\",\n}\nerr := zorm.New[Order]().UpdateManyByKey(ctx, \"reference_number\", \"status\", statusUpdates)\n// Generates: UPDATE orders SET status = CASE reference_number\n//            WHEN 'ORD-001' THEN 'shipped' WHEN 'ORD-002' THEN 'delivered' ... END\n//            WHERE reference_number IN ('ORD-001', 'ORD-002', 'ORD-003')\n\n// Example 2: Update product quantities by product code (int keys, int values)\nquantityUpdates := map[int]int{\n    100: 50,   // product code 100 -\u003e quantity 50\n    200: 75,   // product code 200 -\u003e quantity 75\n    300: 100,  // product code 300 -\u003e quantity 100\n}\nerr = zorm.New[Product]().UpdateManyByKey(ctx, \"code\", \"quantity\", quantityUpdates)\n\n// Example 3: Combine with WHERE clause for conditional updates\n// Only update orders that are in 'pending' status\nstatusUpdates := map[string]string{\n    \"ORD-001\": \"processing\",\n    \"ORD-002\": \"processing\",\n}\nerr = zorm.New[Order]().\n    Where(\"status\", \"pending\").\n    UpdateManyByKey(ctx, \"reference_number\", \"status\", statusUpdates)\n// Only updates if both: reference_number matches AND status = 'pending'\n```\n\n**UpdateManyByKey Features:**\n- Uses efficient CASE WHEN syntax (single query for all updates)\n- Supports any map key/value types (string, int, float64, bool, etc.)\n- Automatically chunks large maps (500+ entries) with transaction safety\n- Combines with existing WHERE conditions\n- Auto-updates `updated_at` timestamp if the column exists\n\n---\n\n## API Reference\n\n### Query Methods\n\n| Method                | Description                           | Returns          |\n| --------------------- | ------------------------------------- | ---------------- |\n| `Get(ctx)`            | Execute query and return all results  | `[]*T, error`    |\n| `First(ctx)`          | Execute query and return first result | `*T, error`      |\n| `Find(ctx, id)`       | Find record by primary key            | `*T, error`      |\n| `FindOrFail(ctx, id)` | Find record or return error           | `*T, error`      |\n| `Exists(ctx)`         | Check if any record matches           | `bool, error`    |\n| `Count(ctx)`          | Count matching records                | `int64, error`   |\n| `Sum(ctx, column)`    | Sum of column values                  | `float64, error` |\n| `Avg(ctx, column)`    | Average of column values              | `float64, error` |\n| `Pluck(ctx, column)`  | Get single column values              | `[]any, error`   |\n\n### Write Methods\n\n| Method                                              | Description                                   |\n| --------------------------------------------------- | --------------------------------------------- |\n| `Create(ctx, entity)`                               | Insert single record                          |\n| `CreateMany(ctx, entities)`                         | Insert multiple records                       |\n| `Update(ctx, entity)`                               | Update single record by primary key           |\n| `UpdateMany(ctx, values)`                           | Update multiple records matching query        |\n| `UpdateManyByKey(ctx, lookup, target, map)`         | Update records by matching lookup column keys |\n| `Delete(ctx)`                                       | Delete records matching query                 |\n| `DeleteMany(ctx)`                                   | Alias for Delete                              |\n| `FirstOrCreate(ctx, attrs, values)`                 | Find first or create new                      |\n| `UpdateOrCreate(ctx, attrs, values)`                | Update existing or create new                 |\n\n### Query Builder Methods\n\n| Method                         | Description               |\n| ------------------------------ | ------------------------- |\n| `Select(columns...)`           | Specify columns to select |\n| `Distinct()`                   | Add DISTINCT to query     |\n| `DistinctBy(columns...)`       | PostgreSQL DISTINCT ON    |\n| `Where(query, args...)`        | Add WHERE condition       |\n| `OrWhere(query, args...)`      | Add OR WHERE condition    |\n| `WhereIn(column, values)`      | WHERE column IN (...)     |\n| `WhereNull(column)`            | WHERE column IS NULL      |\n| `WhereNotNull(column)`         | WHERE column IS NOT NULL  |\n| `OrWhereNull(column)`          | OR column IS NULL         |\n| `OrWhereNotNull(column)`       | OR column IS NOT NULL     |\n| `WhereHas(relation, callback)` | WHERE EXISTS subquery     |\n| `OrderBy(column, direction)`   | Add ORDER BY              |\n| `Latest(column?)`              | ORDER BY column DESC      |\n| `Oldest(column?)`              | ORDER BY column ASC       |\n| `GroupBy(columns...)`          | Add GROUP BY              |\n| `Having(query, args...)`       | Add HAVING                |\n| `Limit(n)`                     | Set LIMIT                 |\n| `Offset(n)`                    | Set OFFSET                |\n| `Lock(mode)`                   | Add FOR UPDATE/SHARE      |\n\n### Utility Methods\n\n| Method                 | Description                 |\n| ---------------------- | --------------------------- |\n| `Clone()`              | Deep copy the query builder |\n| `Table(name)`          | Override table name         |\n| `TableName()`          | Get current table name      |\n| `SetDB(db)`            | Set custom DB connection    |\n| `WithTx(tx)`           | Use transaction             |\n| `WithContext(ctx)`     | Set context                 |\n| `WithStmtCache(cache)` | Enable statement caching    |\n| `Scope(fn)`            | Apply reusable query logic  |\n| `Print()`              | Get SQL without executing   |\n| `Raw(sql, args...)`    | Set raw SQL query           |\n| `Exec(ctx)`            | Execute raw query           |\n\n---\n\n## Query Builder Details\n\n### Where Conditions\n\n```go\n// Equality\nzorm.New[User]().Where(\"name\", \"John\").Get(ctx)\n\n// Operators\nzorm.New[User]().Where(\"age\", \"\u003e\", 18).Get(ctx)\nzorm.New[User]().Where(\"email\", \"LIKE\", \"%@example.com\").Get(ctx)\nzorm.New[User]().Where(\"status\", \"!=\", \"inactive\").Get(ctx)\n\n// Map (multiple AND conditions)\nzorm.New[User]().Where(map[string]any{\n    \"name\": \"John\",\n    \"age\":  25,\n}).Get(ctx)\n\n// Struct (non-zero fields)\nzorm.New[User]().Where(\u0026User{Name: \"John\", Age: 25}).Get(ctx)\n\n// Nested/Grouped conditions\nzorm.New[User]().Where(func(q *zorm.Model[User]) {\n    q.Where(\"role\", \"admin\").OrWhere(\"role\", \"manager\")\n}).Where(\"active\", true).Get(ctx)\n// WHERE (role = 'admin' OR role = 'manager') AND active = true\n\n// NULL checks\nzorm.New[User]().WhereNull(\"deleted_at\").Get(ctx)\nzorm.New[User]().WhereNotNull(\"verified_at\").Get(ctx)\n\n// IN clause\nzorm.New[User]().WhereIn(\"id\", []any{1, 2, 3}).Get(ctx)\n\n// OR conditions\nzorm.New[User]().Where(\"age\", \"\u003e\", 18).OrWhere(\"verified\", true).Get(ctx)\n```\n\n### Exists Check\n\n```go\n// Check if any matching record exists (efficient - uses SELECT 1 LIMIT 1)\nexists, err := zorm.New[User]().Where(\"email\", \"john@example.com\").Exists(ctx)\nif exists {\n    fmt.Println(\"User exists!\")\n}\n```\n\n### Pluck (Single Column)\n\n```go\n// Get just the email column from all users\nemails, err := zorm.New[User]().Where(\"active\", true).Pluck(ctx, \"email\")\nfor _, email := range emails {\n    fmt.Println(email)\n}\n```\n\n### Scalar Queries (Type-Safe Single Column)\n\n`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.\n\n```go\n// Example 1: Get all usernames from users table\nnames, err := zorm.Query[string]().\n    Table(\"users\").\n    Select(\"name\").\n    Where(\"active\", true).\n    Get(ctx)\n// names is []string{\"Alice\", \"Bob\", \"Charlie\"}\n\n// Example 2: Get user IDs ordered by creation date\nids, err := zorm.Query[int64]().\n    Table(\"users\").\n    Select(\"id\").\n    OrderBy(\"created_at\", \"DESC\").\n    Limit(100).\n    Get(ctx)\n// ids is []int64{42, 41, 40, ...}\n\n// Example 3: Get distinct roles with count filtering\nroles, err := zorm.Query[string]().\n    Table(\"users\").\n    Select(\"role\").\n    Distinct().\n    GroupBy(\"role\").\n    Having(\"COUNT(*) \u003e\", 5).\n    Get(ctx)\n// roles is []string{\"admin\", \"editor\"} (roles with more than 5 users)\n```\n\n`ScalarQuery` supports the same query builder methods as `Model`:\n- `Where`, `OrWhere`, `WhereIn`, `WhereNull`, `WhereNotNull`\n- `OrderBy`, `Limit`, `Offset`\n- `Distinct`, `GroupBy`, `Having`\n- `First` (returns single value), `Count` (returns row count)\n- `SetDB`, `WithTx`, `Clone`, `Print`\n\n### Cursor (Memory-Efficient Iteration)\n\nFor large datasets, use `Cursor` to iterate row by row without loading everything into memory:\n\n```go\ncursor, err := zorm.New[User]().Where(\"active\", true).Cursor(ctx)\nif err != nil {\n    return err\n}\ndefer cursor.Close()\n\nfor cursor.Next() {\n    user, err := cursor.Scan(ctx)\n    if err != nil {\n        return err\n    }\n    // Process user one at a time\n    fmt.Println(user.Name)\n}\n```\n\n### FirstOrCreate \u0026 UpdateOrCreate\n\n```go\n// Find first matching record, or create if not found\nuser, err := zorm.New[User]().FirstOrCreate(ctx,\n    map[string]any{\"email\": \"john@example.com\"},  // Search attributes\n    map[string]any{\"name\": \"John\", \"age\": 25},    // Values for creation\n)\n\n// Find and update, or create if not found\nuser, err := zorm.New[User]().UpdateOrCreate(ctx,\n    map[string]any{\"email\": \"john@example.com\"},  // Search attributes\n    map[string]any{\"name\": \"John Updated\"},       // Values to set\n)\n```\n\n### Pagination\n\n```go\n// Full pagination (with total count - 2 queries)\nresult, err := zorm.New[User]().Paginate(ctx, 1, 15)\nfmt.Println(result.Data)        // []*User\nfmt.Println(result.Total)       // Total record count\nfmt.Println(result.CurrentPage) // 1\nfmt.Println(result.LastPage)    // Calculated last page\nfmt.Println(result.PerPage)     // 15\n\n// Simple pagination (no count - 1 query, faster)\nresult, err := zorm.New[User]().SimplePaginate(ctx, 1, 15)\n// result.Total will be -1 (skipped)\n```\n\n### Clone (Reuse Queries Safely)\n\n```go\nbaseQuery := zorm.New[User]().Where(\"active\", true)\n\n// Clone prevents modifying original\nadmins, _ := baseQuery.Clone().Where(\"role\", \"admin\").Get(ctx)\nusers, _ := baseQuery.Clone().Limit(10).Get(ctx)\n\n// Original is unchanged\nall, _ := baseQuery.Get(ctx)\n```\n\n### Custom Table Name\n\n```go\n// Override table name for this query\nusers, _ := zorm.New[User]().Table(\"archived_users\").Get(ctx)\n```\n\n---\n\n## Lifecycle Hooks\n\nZORM supports lifecycle hooks that are automatically called during CRUD operations.\n\n### Available Hooks\n\n| Hook                | When Called   |\n| ------------------- | ------------- |\n| `BeforeCreate(ctx)` | Before INSERT |\n| `BeforeUpdate(ctx)` | Before UPDATE |\n| `AfterUpdate(ctx)`  | After UPDATE  |\n\n### Implementing Hooks\n\n```go\ntype User struct {\n    ID        int64\n    Name      string\n    Email     string\n    CreatedAt time.Time\n    UpdatedAt time.Time\n}\n\n// BeforeCreate is called before inserting a new record\nfunc (u *User) BeforeCreate(ctx context.Context) error {\n    // Validate\n    if u.Email == \"\" {\n        return errors.New(\"email is required\")\n    }\n\n    // Set defaults\n    u.CreatedAt = time.Now()\n\n    // Normalize data\n    u.Email = strings.ToLower(u.Email)\n\n    return nil\n}\n\n// BeforeUpdate is called before updating a record\nfunc (u *User) BeforeUpdate(ctx context.Context) error {\n    // Validate\n    if u.Name == \"\" {\n        return errors.New(\"name cannot be empty\")\n    }\n\n    // updated_at is set automatically by ZORM\n\n    return nil\n}\n\n// AfterUpdate is called after a successful update\nfunc (u *User) AfterUpdate(ctx context.Context) error {\n    // Log, send notifications, update cache, etc.\n    log.Printf(\"User %d updated\", u.ID)\n    return nil\n}\n```\n\n### Hook Execution Flow\n\n```go\n// Create flow:\n// 1. BeforeCreate(ctx) called\n// 2. INSERT executed\n// 3. ID populated\n\nuser := \u0026User{Name: \"John\", Email: \"JOHN@EXAMPLE.COM\"}\nerr := zorm.New[User]().Create(ctx, user)\n// BeforeCreate lowercases email to \"john@example.com\"\n\n// Update flow:\n// 1. updated_at set automatically\n// 2. BeforeUpdate(ctx) called\n// 3. UPDATE executed\n// 4. AfterUpdate(ctx) called\n\nuser.Name = \"Jane\"\nerr = zorm.New[User]().Update(ctx, user)\n```\n\n---\n\n## Accessors (Computed Attributes)\n\nDefine 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.\n\n```go\ntype User struct {\n    ID         int64\n    FirstName  string\n    LastName   string\n    Attributes map[string]any // Holds computed values\n}\n\n// Accessor: GetFullName -\u003e attributes[\"full_name\"]\nfunc (u *User) GetFullName() string {\n    return u.FirstName + \" \" + u.LastName\n}\n\n// Accessor: GetInitials -\u003e attributes[\"initials\"]\nfunc (u *User) GetInitials() string {\n    return string(u.FirstName[0]) + string(u.LastName[0])\n}\n\n// Usage\nuser, _ := zorm.New[User]().Find(ctx, 1)\nfmt.Println(user.Attributes[\"full_name\"])  // \"John Doe\"\nfmt.Println(user.Attributes[\"initials\"])   // \"JD\"\n```\n\n---\n\n## Relationships\n\n### Defining Relations\n\nRelations 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`).\n\n```go\ntype User struct {\n    ID      int64\n    Name    string\n    Posts   []*Post  // HasMany\n    Profile *Profile // HasOne\n}\n\n// HasMany: User has many Posts\n// Method can be named \"Posts\" or \"PostsRelation\"\nfunc (u User) PostsRelation() zorm.HasMany[Post] {\n    return zorm.HasMany[Post]{\n        ForeignKey: \"user_id\",  // Column in posts table\n        LocalKey:   \"id\",       // Optional, defaults to primary key\n    }\n}\n\n// HasOne: User has one Profile\nfunc (u User) ProfileRelation() zorm.HasOne[Profile] {\n    return zorm.HasOne[Profile]{\n        ForeignKey: \"user_id\",\n    }\n}\n\ntype Post struct {\n    ID     int64\n    UserID int64\n    Title  string\n    Author *User    // BelongsTo\n}\n\n// BelongsTo: Post belongs to User\nfunc (p Post) AuthorRelation() zorm.BelongsTo[User] {\n    return zorm.BelongsTo[User]{\n        ForeignKey: \"user_id\",  // Column in posts table\n        OwnerKey:   \"id\",       // Optional, defaults to primary key\n    }\n}\n```\n\n### Custom Table Names in Relations\n\n```go\nfunc (u User) PostsRelation() zorm.HasMany[Post] {\n    return zorm.HasMany[Post]{\n        ForeignKey: \"user_id\",\n        Table:      \"blog_posts\",  // Use custom table name\n    }\n}\n```\n\n### Eager Loading\n\n```go\n// Load single relation (use the relation name without \"Relation\" suffix)\nusers, _ := zorm.New[User]().With(\"Posts\").Get(ctx)\n\n// Load multiple relations\nusers, _ := zorm.New[User]().With(\"Posts\", \"Profile\").Get(ctx)\n\n// Load nested relations\nusers, _ := zorm.New[User]().With(\"Posts.Comments\").Get(ctx)\n\n// Load with constraints\nusers, _ := zorm.New[User]().WithCallback(\"Posts\", func(q *zorm.Model[Post]) {\n    q.Where(\"published\", true).\n      OrderBy(\"created_at\", \"DESC\").\n      Limit(5)\n}).Get(ctx)\n```\n\n### Lazy Loading\n\n```go\nuser, _ := zorm.New[User]().Find(ctx, 1)\n\n// Load relation on existing entity\nerr := zorm.New[User]().Load(ctx, user, \"Posts\")\n\n// Load on slice\nusers, _ := zorm.New[User]().Get(ctx)\nerr := zorm.New[User]().LoadSlice(ctx, users, \"Posts\", \"Profile\")\n```\n\n### Many-to-Many Relations\n\n```go\ntype User struct {\n    ID    int64\n    Roles []*Role\n}\n\nfunc (u User) RolesRelation() zorm.BelongsToMany[Role] {\n    return zorm.BelongsToMany[Role]{\n        PivotTable: \"role_user\",   // Join table\n        ForeignKey: \"user_id\",     // FK in pivot table\n        RelatedKey: \"role_id\",     // Related FK in pivot table\n    }\n}\n```\n\n#### Managing Many-to-Many Associations\n\nZORM provides three methods to manage pivot table associations: `Attach`, `Detach`, and `Sync`.\n\n```go\nuser := \u0026User{ID: 1}\n\n// Attach - Add new associations (inserts into pivot table)\nerr := zorm.New[User]().Attach(ctx, user, \"Roles\", []any{3, 4}, nil)\n// Adds role_user entries: (1,3), (1,4)\n\n// Attach with pivot data (extra columns in pivot table)\npivotData := map[any]map[string]any{\n    3: {\"assigned_at\": time.Now(), \"assigned_by\": 1},\n    4: {\"assigned_at\": time.Now(), \"assigned_by\": 1},\n}\nerr = zorm.New[User]().Attach(ctx, user, \"Roles\", []any{3, 4}, pivotData)\n\n// Detach - Remove specific associations\nerr = zorm.New[User]().Detach(ctx, user, \"Roles\", []any{2})\n// Removes role_user entry: (1,2)\n\n// Detach all - Remove all associations for the relation\nerr = zorm.New[User]().Detach(ctx, user, \"Roles\", nil)\n// Removes all role_user entries where user_id = 1\n```\n\n#### Sync - Synchronize Associations\n\n`Sync` is a handy method for managing many-to-many relations. It synchronizes the pivot table to match exactly the IDs you provide:\n- **Attaches** IDs that are in the new list but not in the database\n- **Detaches** IDs that are in the database but not in the new list\n- **Keeps** IDs that exist in both (no duplicate entry errors)\n\n```go\nuser := \u0026User{ID: 1}\n// Current roles in DB: [1, 2, 3]\n\n// Sync to new set of roles\nerr := zorm.New[User]().Sync(ctx, user, \"Roles\", []any{1, 2, 4}, nil)\n// Result:\n// - Role 1: kept (exists in both)\n// - Role 2: kept (exists in both)\n// - Role 3: detached (was in DB, not in new list)\n// - Role 4: attached (not in DB, is in new list)\n// Final roles in DB: [1, 2, 4]\n\n// Sync with pivot data for new attachments\npivotData := map[any]map[string]any{\n    4: {\"assigned_at\": time.Now()},\n}\nerr = zorm.New[User]().Sync(ctx, user, \"Roles\", []any{1, 2, 4}, pivotData)\n```\n\n**Common Sync Use Cases:**\n\n```go\n// Replace all user roles with a new set\nerr := zorm.New[User]().Sync(ctx, user, \"Roles\", []any{1, 2}, nil)\n\n// Remove all roles (sync with empty list)\nerr = zorm.New[User]().Sync(ctx, user, \"Roles\", []any{}, nil)\n\n// Form submission: update user roles from checkbox selection\nselectedRoleIDs := []any{1, 3, 5}  // From form\nerr = zorm.New[User]().Sync(ctx, user, \"Roles\", selectedRoleIDs, nil)\n```\n\n### Polymorphic Relations\n\n```go\ntype Image struct {\n    ID            int64\n    URL           string\n    ImageableType string  // \"users\" or \"posts\"\n    ImageableID   int64\n}\n\n// MorphOne: User has one Image\nfunc (u User) AvatarRelation() zorm.MorphOne[Image] {\n    return zorm.MorphOne[Image]{\n        Type: \"ImageableType\",  // Type column\n        ID:   \"ImageableID\",    // ID column\n    }\n}\n\n// MorphMany: Post has many Images\nfunc (p Post) ImagesRelation() zorm.MorphMany[Image] {\n    return zorm.MorphMany[Image]{\n        Type: \"ImageableType\",\n        ID:   \"ImageableID\",\n    }\n}\n\n// Loading with type constraints\nimages, _ := zorm.New[Image]().WithMorph(\"Imageable\", map[string][]string{\n    \"users\": {\"Profile\"},  // When type=users, also load Profile\n    \"posts\": {},           // When type=posts, just load Post\n}).Get(ctx)\n```\n\n---\n\n## Transactions\n\n```go\n// Function-based transaction\nerr := zorm.Transaction(ctx, func(tx *zorm.Tx) error {\n    user := \u0026User{Name: \"John\"}\n    if err := zorm.New[User]().WithTx(tx).Create(ctx, user); err != nil {\n        return err // Rollback\n    }\n\n    post := \u0026Post{UserID: user.ID, Title: \"First Post\"}\n    if err := zorm.New[Post]().WithTx(tx).Create(ctx, post); err != nil {\n        return err // Rollback\n    }\n\n    return nil // Commit\n})\n\n// Model-based transaction\nerr = zorm.New[User]().Transaction(ctx, func(tx *zorm.Tx) error {\n    return zorm.New[User]().WithTx(tx).Create(ctx, \u0026User{Name: \"Jane\"})\n})\n```\n\nTransaction features:\n\n- Auto-rollback on error return\n- Auto-rollback on panic (re-panics after rollback)\n- Auto-commit on nil return\n\n---\n\n## Error Handling\n\nZORM provides comprehensive error handling with categorized errors.\n\n### Sentinel Errors\n\n```go\nimport \"github.com/rezakhademix/zorm\"\n\n// Query errors\nzorm.ErrRecordNotFound     // No matching record\n\n// Model errors\nzorm.ErrInvalidModel       // Invalid model type\nzorm.ErrNilPointer         // Nil pointer passed\n\n// Relation errors\nzorm.ErrRelationNotFound   // Relation method not found\nzorm.ErrInvalidRelation    // Invalid relation type\n\n// Constraint violations\nzorm.ErrDuplicateKey       // Unique constraint violation\nzorm.ErrForeignKey         // Foreign key constraint violation\nzorm.ErrNotNullViolation   // NOT NULL constraint violation\nzorm.ErrCheckViolation     // CHECK constraint violation\n\n// Connection errors\nzorm.ErrConnectionFailed   // Connection refused\nzorm.ErrConnectionLost     // Connection lost during operation\nzorm.ErrTimeout            // Operation timeout\n\n// Transaction errors\nzorm.ErrTransactionDeadlock    // Deadlock detected\nzorm.ErrSerializationFailure  // Serialization failure\n\n// Schema errors\nzorm.ErrColumnNotFound     // Column doesn't exist\nzorm.ErrTableNotFound      // Table doesn't exist\nzorm.ErrInvalidSyntax      // SQL syntax error\n```\n\n### Error Helper Functions\n\n```go\nuser, err := zorm.New[User]().Find(ctx, 999)\n\n// Check specific error types\nif zorm.IsNotFound(err) {\n    // Handle not found\n}\n\nif zorm.IsDuplicateKey(err) {\n    // Handle duplicate\n}\n\nif zorm.IsConstraintViolation(err) {\n    // Any constraint violation\n}\n\nif zorm.IsConnectionError(err) {\n    // Connection failed or lost\n}\n\nif zorm.IsTimeout(err) {\n    // Operation timed out\n}\n\nif zorm.IsDeadlock(err) {\n    // Transaction deadlock - retry\n}\n\nif zorm.IsSchemaError(err) {\n    // Missing column, table, or syntax error\n}\n```\n\n### QueryError Details\n\n```go\nuser, err := zorm.New[User]().Create(ctx, \u0026User{Email: \"duplicate@example.com\"})\nif err != nil {\n    if qe := zorm.GetQueryError(err); qe != nil {\n        fmt.Println(qe.Query)      // The SQL that failed\n        fmt.Println(qe.Args)       // Query arguments\n        fmt.Println(qe.Operation)  // \"INSERT\", \"SELECT\", etc.\n        fmt.Println(qe.Table)      // Table name (if detected)\n        fmt.Println(qe.Constraint) // Constraint name (if detected)\n    }\n}\n```\n\n---\n\n## Advanced Features\n\n### Statement Caching\n\nImprove performance by reusing prepared statements:\n\n```go\ncache := zorm.NewStmtCache(100)  // Cache up to 100 statements\ndefer cache.Close()\n\nmodel := zorm.New[User]().WithStmtCache(cache)\n\n// Statements are prepared once and reused\nusers, _ := model.Clone().Where(\"age\", \"\u003e\", 18).Get(ctx)\nusers, _ := model.Clone().Where(\"age\", \"\u003e\", 25).Get(ctx)  // Reuses prepared statement\n```\n\n### Read/Write Splitting\n\n```go\n// Configure resolver\nzorm.ConfigureDBResolver(\n    zorm.WithPrimary(primaryDB),\n    zorm.WithReplicas(replica1, replica2),\n    zorm.WithLoadBalancer(zorm.RoundRobinLB),\n)\n\n// Automatic routing\nusers, _ := zorm.New[User]().Get(ctx)          // Reads from replica\nerr := zorm.New[User]().Create(ctx, user)      // Writes to primary\n\n// Force primary for consistency\nusers, _ := zorm.New[User]().UsePrimary().Get(ctx)\n\n// Force specific replica\nusers, _ := zorm.New[User]().UseReplica(0).Get(ctx)\n```\n\n### Common Table Expressions (CTEs)\n\n```go\n// String CTE\nusers, _ := zorm.New[User]().\n    WithCTE(\"active_users\", \"SELECT * FROM users WHERE active = true\").\n    Raw(\"SELECT * FROM active_users WHERE age \u003e 18\").\n    Get(ctx)\n\n// Subquery CTE\nsubQuery := zorm.New[User]().Where(\"active\", true)\nusers, _ := zorm.New[User]().\n    WithCTE(\"active_users\", subQuery).\n    Raw(\"SELECT * FROM active_users\").\n    Get(ctx)\n```\n\n### Full-Text Search (PostgreSQL)\n\n```go\n// Basic full-text search\narticles, _ := zorm.New[Article]().\n    WhereFullText(\"content\", \"database sql\").Get(ctx)\n\n// With language config\narticles, _ := zorm.New[Article]().\n    WhereFullTextWithConfig(\"content\", \"base de datos\", \"spanish\").Get(ctx)\n\n// Pre-computed tsvector column (fastest)\narticles, _ := zorm.New[Article]().\n    WhereTsVector(\"search_vector\", \"golang \u0026 performance\").Get(ctx)\n\n// Phrase search (word order matters)\narticles, _ := zorm.New[Article]().\n    WherePhraseSearch(\"title\", \"getting started\").Get(ctx)\n```\n\n### Row Locking\n\n```go\n// Lock for update (exclusive)\nuser, _ := zorm.New[User]().Where(\"id\", 1).Lock(\"UPDATE\").First(ctx)\n\n// Shared lock\nuser, _ := zorm.New[User]().Where(\"id\", 1).Lock(\"SHARE\").First(ctx)\n\n// PostgreSQL-specific\nuser, _ := zorm.New[User]().Where(\"id\", 1).Lock(\"NO KEY UPDATE\").First(ctx)\n```\n\n### Advanced Grouping\n\n```go\n// ROLLUP\nzorm.New[Order]().\n    Select(\"region\", \"city\", \"SUM(amount)\").\n    GroupByRollup(\"region\", \"city\").Get(ctx)\n\n// CUBE\nzorm.New[Order]().\n    Select(\"year\", \"month\", \"SUM(amount)\").\n    GroupByCube(\"year\", \"month\").Get(ctx)\n\n// GROUPING SETS\nzorm.New[Order]().\n    GroupByGroupingSets(\n        []string{\"region\"},\n        []string{\"city\"},\n        []string{},  // Grand total\n    ).Get(ctx)\n```\n\n### Chunking Large Datasets\n\n```go\nerr := zorm.New[User]().Chunk(ctx, 1000, func(users []*User) error {\n    for _, user := range users {\n        // Process each user\n    }\n    return nil  // Return error to stop chunking\n})\n```\n\n### Scopes (Reusable Query Logic)\n\n```go\nfunc Active(q *zorm.Model[User]) *zorm.Model[User] {\n    return q.Where(\"active\", true).WhereNull(\"deleted_at\")\n}\n\nfunc Verified(q *zorm.Model[User]) *zorm.Model[User] {\n    return q.WhereNotNull(\"verified_at\")\n}\n\nfunc RecentlyActive(q *zorm.Model[User]) *zorm.Model[User] {\n    return q.Where(\"last_login\", \"\u003e\", time.Now().AddDate(0, -1, 0))\n}\n\n// Chain scopes\nusers, _ := zorm.New[User]().\n    Scope(Active).\n    Scope(Verified).\n    Scope(RecentlyActive).\n    Get(ctx)\n```\n\n### Query Debugging\n\n```go\nsql, args := zorm.New[User]().\n    Where(\"age\", \"\u003e\", 18).\n    OrderBy(\"name\", \"ASC\").\n    Limit(10).\n    Print()\n\nfmt.Println(sql)   // SELECT * FROM users WHERE 1=1 AND age \u003e ? ORDER BY name ASC LIMIT 10\nfmt.Println(args)  // [18]\n```\n\n---\n\n## Complete Example\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n    \"time\"\n\n    \"github.com/rezakhademix/zorm\"\n)\n\ntype User struct {\n    ID        int64\n    Name      string\n    Email     string\n    Age       int\n    Active    bool\n    CreatedAt time.Time\n    UpdatedAt time.Time\n    Posts     []*Post\n}\n\nfunc (u *User) BeforeCreate(ctx context.Context) error {\n    u.CreatedAt = time.Now()\n    u.Active = true\n    return nil\n}\n\nfunc (u User) PostsRelation() zorm.HasMany[Post] {\n    return zorm.HasMany[Post]{ForeignKey: \"user_id\"}\n}\n\ntype Post struct {\n    ID        int64\n    UserID    int64\n    Title     string\n    Published bool\n}\n\nfunc main() {\n    ctx := context.Background()\n\n    // Connect\n    db, err := zorm.ConnectPostgres(\"postgres://...\", nil)\n    if err != nil {\n        log.Fatal(err)\n    }\n    zorm.GlobalDB = db\n\n    // Create with hook\n    user := \u0026User{Name: \"John\", Email: \"john@example.com\", Age: 25}\n    if err := zorm.New[User]().Create(ctx, user); err != nil {\n        log.Fatal(err)\n    }\n    fmt.Printf(\"Created user %d\\n\", user.ID)\n\n    // Query with relations\n    users, err := zorm.New[User]().\n        Where(\"age\", \"\u003e\", 18).\n        Where(\"active\", true).\n        WithCallback(\"Posts\", func(q *zorm.Model[Post]) {\n            q.Where(\"published\", true).Limit(5)\n        }).\n        OrderBy(\"created_at\", \"DESC\").\n        Limit(10).\n        Get(ctx)\n\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    for _, u := range users {\n        fmt.Printf(\"%s has %d published posts\\n\", u.Name, len(u.Posts))\n    }\n\n    // FirstOrCreate\n    user, err = zorm.New[User]().FirstOrCreate(ctx,\n        map[string]any{\"email\": \"jane@example.com\"},\n        map[string]any{\"name\": \"Jane\", \"age\": 30},\n    )\n\n    // Pagination\n    result, _ := zorm.New[User]().Paginate(ctx, 1, 15)\n    fmt.Printf(\"Page 1 of %d, Total: %d\\n\", result.LastPage, result.Total)\n}\n```\n\n---\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\nMIT License - see LICENSE file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frezakhademix%2Fzorm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frezakhademix%2Fzorm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frezakhademix%2Fzorm/lists"}