relica

package module
v0.14.1 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 10 Imported by: 1

README

Relica

CI codecov Go Version Go Report Card License Release Go Reference

Relica is a lightweight, type-safe database query builder for Go with zero production dependencies.

🤖 AI Agents: Before generating code, read AGENTS.md for correct API patterns. Use Model() API for CRUD, Expression API for WHERE conditions. Avoid map[string]interface{}.

✨ Features

  • Zero Production Dependencies - Uses only Go standard library
  • High Performance - LRU statement cache, batch operations (3.3x faster)
  • Type-Safe - Reflection-based struct scanning with compile-time checks
  • Model() API - ORM-style CRUD with auto-populated IDs, composite PKs (ozzo-dbx compatible)
  • NullStringMap - Dynamic scanning without predefined structs
  • Named Placeholders - {:name} syntax with Bind(Params{}) for readable queries
  • Functional Expressions - CASE, COALESCE, NULLIF, GREATEST, LEAST, CONCAT
  • Transactional() - Auto commit/rollback helper with panic recovery
  • Dynamic WHERE - AndWhere() / OrWhere() for conditional query building
  • Row() / Column() - Convenient scalar and single-column queries
  • Prepare() / Close() - Manual statement control for batch operations
  • Transaction Support - Full ACID with all isolation levels
  • Enterprise Security - SQL injection prevention, audit logging, compliance
  • Batch Operations - Efficient multi-row INSERT and UPDATE
  • JOIN Operations - INNER, LEFT, RIGHT, FULL, CROSS JOIN support
  • Sorting & Pagination - ORDER BY, LIMIT, OFFSET, DISTINCT
  • Aggregate Functions - COUNT, SUM, AVG, MIN, MAX, GROUP BY, HAVING
  • Subqueries - IN, EXISTS, FROM subqueries, scalar subqueries
  • Set Operations - UNION, UNION ALL, INTERSECT, EXCEPT
  • Common Table Expressions - WITH clause, recursive CTEs
  • Multi-Database - PostgreSQL, MySQL 8.0+, SQLite 3.25+ support
  • Well-Tested - 1500+ test cases, 88%+ coverage
  • Clean API - Fluent builder pattern with context support

Latest Release: See CHANGELOG.md for version history and GitHub Releases for release notes.

📌 API Usage Priority

Priority API When to Use
PREFERRED db.Model(&struct).Insert/Update/Delete/Upsert() All CRUD operations with structs
PREFERRED relica.Eq(), relica.And(), relica.In(), etc. WHERE conditions
PREFERRED relica.HashExp{"col": val} Simple equality conditions
ACCEPTABLE Where("col = {:col}", relica.Params{"col": val}) Named placeholders
ACCEPTABLE Where("col = ?", val) Positional placeholders
AVOID map[string]interface{} Only for dynamic/unknown schemas

For AI Agents: See AGENTS.md for complete patterns and examples.

🚀 Quick Start

Installation
go get github.com/coregx/relica

Note: Always import only the main relica package. Internal packages are protected and not part of the public API.

Basic Usage
package main

import (
    "context"
    "fmt"
    "log"

    "github.com/coregx/relica"
    _ "github.com/lib/pq" // PostgreSQL driver
)

type User struct {
    ID    int    `db:"id"`
    Name  string `db:"name"`
    Email string `db:"email"`
}

func main() {
    // Connect to database
    db, err := relica.Open("postgres", "postgres://user:pass@localhost/db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    ctx := context.Background()

    // SELECT with Expression API (PREFERRED)
    var user User
    err = db.Select().
        From("users").
        Where(relica.Eq("id", 1)).
        WithContext(ctx).
        One(&user)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("User: %+v\n", user)

    // SELECT with multiple conditions (PREFERRED)
    var users []User
    err = db.Select().
        From("users").
        Where(relica.And(
            relica.GreaterThan("age", 18),
            relica.Eq("status", "active"),
        )).
        All(&users)

    // INSERT with Model() API (PREFERRED)
    newUser := User{Name: "Alice", Email: "alice@example.com"}
    err = db.Model(&newUser).Insert()
    fmt.Println(newUser.ID) // Auto-populated!

    // UPDATE with Model() API (PREFERRED)
    newUser.Name = "Alice Updated"
    err = db.Model(&newUser).Update()

    // DELETE with Model() API (PREFERRED)
    err = db.Model(&newUser).Delete()

    // CTE (Common Table Expression)
    statsQuery := db.Select("user_id", "COUNT(*) as order_count").
        From("orders").
        GroupBy("user_id")

    var results []struct {
        UserID     int `db:"user_id"`
        OrderCount int `db:"order_count"`
    }
    err = db.Select().
        With("stats", statsQuery).
        From("stats").
        All(&results)
}

📚 Core Features

CRUD Operations
Model() API (PREFERRED)

Use Model() API for all struct-based CRUD operations:

// INSERT - Auto-populates ID (PREFERRED)
user := User{Name: "Bob", Email: "bob@example.com"}
db.Model(&user).Insert()
fmt.Println(user.ID) // Auto-populated!

// INSERT - Selective fields
db.Model(&user).Insert("name", "email") // Only these fields

// UPDATE - By primary key (PREFERRED)
user.Status = "inactive"
db.Model(&user).Update()

// UPDATE - Selective fields
db.Model(&user).Update("status") // Only update status

// DELETE - By primary key (PREFERRED)
db.Model(&user).Delete()
SELECT with Expression API (PREFERRED)
// Simple equality
db.Select().From("users").
    Where(relica.Eq("id", 1)).
    One(&user)

// Multiple conditions
db.Select().From("users").
    Where(relica.And(
        relica.GreaterThan("age", 18),
        relica.Eq("status", "active"),
    )).
    All(&users)

// HashExp for simple equality
db.Select().From("users").
    Where(relica.HashExp{"status": "active", "role": "admin"}).
    All(&users)
Map-based Operations (AVOID - Use Only for Dynamic Data)

Warning: Use map[string]interface{} ONLY when struct is not available (dynamic schemas, JSON payloads).

// AVOID - Only for dynamic/unknown schemas
db.Insert("users", map[string]interface{}{
    "name": dynamicData["name"],
}).Execute()

// PREFER - Use Model() API instead
user := User{Name: dynamicData["name"].(string)}
db.Model(&user).Insert()
Expression API

Relica supports fluent expression builders for type-safe, complex WHERE clauses:

HashExp - Simple Conditions
// Simple equality
db.Select().From("users").
    Where(relica.HashExp{"status": 1}).
    All(&users)

// Multiple conditions (AND)
db.Select().From("users").
    Where(relica.HashExp{
        "status": 1,
        "age":    30,
    }).
    All(&users)

// IN clause (slice values)
db.Select().From("users").
    Where(relica.HashExp{
        "status": []interface{}{1, 2, 3},
    }).
    All(&users)

// NULL handling
db.Select().From("users").
    Where(relica.HashExp{
        "deleted_at": nil,  // IS NULL
    }).
    All(&users)

// Combined: IN + NULL + equality
db.Select().From("users").
    Where(relica.HashExp{
        "status":     []interface{}{1, 2},
        "deleted_at": nil,
        "role":       "admin",
    }).
    All(&users)
Comparison Operators
// Greater than
db.Select().From("users").
    Where(relica.GreaterThan("age", 18)).
    All(&users)

// Less than or equal
db.Select().From("users").
    Where(relica.LessOrEqual("price", 100.0)).
    All(&products)

// Available: Eq, NotEq, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual
IN and BETWEEN
// IN
db.Select().From("users").
    Where(relica.In("role", "admin", "moderator")).
    All(&users)

// NOT IN
db.Select().From("users").
    Where(relica.NotIn("status", 0, 99)).
    All(&users)

// BETWEEN
db.Select().From("orders").
    Where(relica.Between("created_at", startDate, endDate)).
    All(&orders)
LIKE with Automatic Escaping
// Default: %value% (partial match)
db.Select().From("users").
    Where(relica.Like("name", "john")).  // name LIKE '%john%'
    All(&users)

// Multiple values (AND)
db.Select().From("articles").
    Where(relica.Like("title", "go", "database")).  // title LIKE '%go%' AND title LIKE '%database%'
    All(&articles)

// Custom matching (prefix/suffix)
db.Select().From("files").
    Where(relica.Like("filename", ".txt").Match(false, true)).  // filename LIKE '%.txt'
    All(&files)

// OR logic
db.Select().From("users").
    Where(relica.OrLike("email", "gmail", "yahoo")).  // email LIKE '%gmail%' OR email LIKE '%yahoo%'
    All(&users)
Logical Combinators
// AND
db.Select().From("users").
    Where(relica.And(
        relica.Eq("status", 1),
        relica.GreaterThan("age", 18),
    )).
    All(&users)

// OR
db.Select().From("users").
    Where(relica.Or(
        relica.Eq("role", "admin"),
        relica.Eq("role", "moderator"),
    )).
    All(&users)

// NOT
db.Select().From("users").
    Where(relica.Not(
        relica.In("status", 0, 99),
    )).
    All(&users)

// Nested combinations
db.Select().From("users").
    Where(relica.And(
        relica.Eq("status", 1),
        relica.Or(
            relica.Eq("role", "admin"),
            relica.GreaterThan("age", 30),
        ),
    )).
    All(&users)
Named Placeholders

Named parameters use {:name} syntax with relica.Params map — readable, safe, and reusable:

// Single parameter
db.Select().From("users").
    Where("id = {:id}", relica.Params{"id": 1}).
    One(&user)

// Multiple parameters
db.Select().From("users").
    Where("status = {:status} AND role = {:role}", relica.Params{
        "status": "active",
        "role":   "admin",
    }).
    All(&users)

// Same parameter used multiple times
db.Select().From("categories").
    Where("parent_id = {:id} OR id = {:id}", relica.Params{"id": categoryID}).
    All(&categories)

Named placeholders work in Where, AndWhere, OrWhere on Select, Update, and Delete queries.

Positional Placeholders

Positional ? parameters also work (backward compatible):

db.Select().From("users").
    Where("status = ? AND age > ?", 1, 18).
    All(&users)

// Can mix styles
db.Select().From("users").
    Where("status = ?", 1).
    Where(relica.GreaterThan("age", 18)).
    All(&users)
Model() API

ORM-style operations with automatic struct mapping, auto-populated IDs, and selective field control.

Basic CRUD Operations
type User struct {
    ID    int64  `db:"id"`     // Auto-populated after INSERT
    Name  string `db:"name"`
    Email string `db:"email"`
    Status string `db:"status"`
}

// INSERT - Auto-populates ID after insert
user := User{Name: "Alice", Email: "alice@example.com"}
err := db.Model(&user).Insert()
fmt.Println(user.ID) // 1 (auto-populated!)

// INSERT - Selective fields
user := User{Name: "Bob", Email: "bob@example.com", Status: "pending"}
err := db.Model(&user).Insert("name", "email") // Only name and email inserted

// UPDATE - Updates all fields by primary key
user.Name = "Alice Updated"
err := db.Model(&user).Update()

// UPDATE - Selective fields
err := db.Model(&user).Update("status") // Only update status field

// DELETE - By primary key
err := db.Model(&user).Delete()
Auto-Populate ID

Works across all databases (PostgreSQL, MySQL, SQLite):

user := User{Name: "Charlie"}
err := db.Model(&user).Insert()

// PostgreSQL: Uses RETURNING clause
// MySQL/SQLite: Uses LastInsertId()
// ID automatically populated in all cases
fmt.Println(user.ID) // Auto-generated ID
Selective Fields

Control exactly which fields are inserted/updated:

user := User{
    Name:   "Alice",
    Email:  "alice@example.com",
    Status: "pending",
}

// Insert only name and email (status remains default)
db.Model(&user).Insert("name", "email")

// Update only status (name and email unchanged)
user.Status = "active"
db.Model(&user).Update("status")

// Exclude fields (Exclude takes precedence)
db.Model(&user).Exclude("status").Insert("name", "email", "status")
// Result: Only name and email inserted
Primary Key Detection

Priority order:

  1. Field tagged db:"pk" — legacy single PK (maps to column "pk")
  2. Field tagged db:"column,pk" — explicit PK with column name
  3. Field named ID (no tag) — auto-detected
  4. Field named Id (no tag) — last resort
type Product struct {
    ProductID int    `db:"product_id,pk"` // Explicit PK with column name
    Name      string `db:"name"`
}

type Order struct {
    ID int // Detected automatically (field named "ID")
}
Transactions
tx, err := db.Begin(ctx)
defer tx.Rollback()

user := User{Name: "Alice"}
err = tx.Model(&user).Insert()

if err == nil {
    tx.Commit()
}
Advanced Usage
// Custom table name
db.Model(&user).Table("custom_users").Insert()

// Exclude fields from operation
db.Model(&user).Exclude("created_at", "updated_at").Update()

// Pre-set IDs (won't be overwritten)
user := User{ID: 999, Name: "System"}
db.Model(&user).Insert() // ID stays 999
Upsert (INSERT or UPDATE)
// INSERT ... ON CONFLICT (id) DO UPDATE SET all non-PK fields
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
err := db.Model(&user).Upsert()

// Selective: only update specific fields on conflict
err = db.Model(&user).Upsert("name", "email")

Works with PostgreSQL (ON CONFLICT DO UPDATE), MySQL (ON DUPLICATE KEY UPDATE), SQLite (ON CONFLICT DO UPDATE).

UpdateChanged (Dirty Field Detection)
// Load user
var user User
db.Select().From("users").Where(relica.Eq("id", 1)).One(&user)

// Save original, modify fields
original := user
user.Name = "Alice Updated"
user.Status = 2

// UPDATE only changed fields (name, status) — not all fields
err := db.Model(&user).UpdateChanged(&original)
// Generates: UPDATE users SET name=?, status=? WHERE id=?

// If nothing changed — returns nil, no query executed
JOIN Operations

Solve N+1 query problems with JOIN support - reduces 101 queries to 1 query (100x improvement).

// Simple INNER JOIN
var results []struct {
    UserID   int    `db:"user_id"`
    UserName string `db:"user_name"`
    PostID   int    `db:"post_id"`
    Title    string `db:"title"`
}

db.Select("u.id as user_id", "u.name as user_name", "p.id as post_id", "p.title").
    From("users u").
    InnerJoin("posts p", "p.user_id = u.id").
    All(&results)

// Multiple JOINs with aggregates
db.Select("messages.*", "users.name", "COUNT(attachments.id) as attachment_count").
    From("messages m").
    InnerJoin("users u", "m.user_id = u.id").
    LeftJoin("attachments a", "m.id = a.message_id").
    Where("m.status = ?", 1).
    GroupBy("messages.id").
    All(&results)

// All JOIN types supported
db.Select().From(table1).InnerJoin(table2, on)  // INNER JOIN
db.Select().From(table1).LeftJoin(table2, on)   // LEFT OUTER JOIN
db.Select().From(table1).RightJoin(table2, on)  // RIGHT OUTER JOIN
db.Select().From(table1).FullJoin(table2, on)   // FULL OUTER JOIN (PostgreSQL, SQLite)
db.Select().From(table1).CrossJoin(table2)      // CROSS JOIN (no ON condition)

// JOIN with Expression API
db.Select().
    From("messages m").
    InnerJoin("users u", relica.And(
        relica.NewExp("m.user_id = u.id"),
        relica.GreaterThan("u.status", 0),
    )).
    All(&results)

Performance: 100x query reduction (N+1 problem solved), 6-25x faster depending on database.

See the Advanced Patterns Guide for comprehensive JOIN examples and best practices.

Sorting and Pagination

Database-side sorting and pagination for efficient data retrieval - 100x memory reduction.

// ORDER BY with multiple columns
db.Select().
    From("messages").
    OrderBy("created_at DESC", "id ASC").
    All(&messages)

// Pagination with LIMIT and OFFSET
const pageSize = 100
const pageNumber = 2 // Third page (0-indexed)

db.Select().
    From("users").
    OrderBy("age DESC").
    Limit(pageSize).
    Offset(pageNumber * pageSize).
    All(&users)

// Table column references
db.Select().
    From("messages m").
    InnerJoin("users u", "m.user_id = u.id").
    OrderBy("m.created_at DESC", "u.name ASC").
    Limit(50).
    All(&results)

Performance: 100x memory reduction (fetch only what you need vs all rows), 6x faster.

Aggregate Functions

Database-side aggregations for COUNT, SUM, AVG, MIN, MAX - 2,500,000x memory reduction.

// Simple COUNT
var count struct{ Total int `db:"total"` }
db.Select("COUNT(*) as total").
    From("messages").
    One(&count)

// Multiple aggregates
type Stats struct {
    Count int     `db:"count"`
    Sum   int64   `db:"sum"`
    Avg   float64 `db:"avg"`
    Min   int     `db:"min"`
    Max   int     `db:"max"`
}

var stats Stats
db.Select("COUNT(*) as count", "SUM(size) as sum", "AVG(size) as avg", "MIN(size) as min", "MAX(size) as max").
    From("messages").
    One(&stats)

// GROUP BY with HAVING
type UserStats struct {
    UserID       int `db:"user_id"`
    MessageCount int `db:"message_count"`
}

var userStats []UserStats
db.Select("user_id", "COUNT(*) as message_count").
    From("messages").
    GroupBy("user_id").
    Having("COUNT(*) > ?", 100).
    OrderBy("message_count DESC").
    All(&userStats)

Performance: 2,500,000x memory reduction (database aggregation vs fetching all rows), 20x faster.

See the Advanced Patterns Guide for comprehensive aggregate examples and patterns.

Query Helpers
Exists
exists, err := db.Select().From("users").
    Where(relica.Eq("email", "alice@example.com")).
    Exists()
// SELECT EXISTS(SELECT 1 FROM "users" WHERE "email" = $1)
Count
count, err := db.Select().From("users").
    Where(relica.Eq("status", "active")).
    Count()
// SELECT COUNT(*) FROM "users" WHERE "status" = $1
ToSQL (Query Preview)
// Preview SQL without executing
sql, params := db.Select().From("users").
    Where(relica.Eq("id", 1)).
    ToSQL()
// sql = `SELECT * FROM "users" WHERE "id" = $1`
// params = []interface{}{1}

// Works on Update and Delete too
sql, params = db.Update("users").
    Set(map[string]interface{}{"status": "active"}).
    Where(relica.Eq("id", 1)).
    ToSQL()
Error Handling
ErrNotFound

One() returns relica.ErrNotFound instead of raw sql.ErrNoRows:

var user User
err := db.Select().From("users").Where(relica.Eq("id", 999)).One(&user)

if errors.Is(err, relica.ErrNotFound) {
    // record not found — clean, library-level error
}

// sql.ErrNoRows still works too (wrapped inside)
if errors.Is(err, sql.ErrNoRows) {
    // also true
}

Note: All() returns empty slice for no results, not an error.

Error Classification

Database-agnostic error helpers — work with PostgreSQL, MySQL, and SQLite:

_, err := db.Model(&user).Insert()

if relica.IsUniqueViolation(err) {
    // duplicate key (e.g., email already exists)
}
if relica.IsForeignKeyViolation(err) {
    // referenced record doesn't exist
}
if relica.IsNotNullViolation(err) {
    // required field is missing
}
if relica.IsCheckViolation(err) {
    // CHECK constraint failed
}
Advanced SQL Features

Relica adds powerful SQL features for complex queries.

Subqueries

IN/EXISTS Subqueries:

// Find users who have placed orders
sub := db.Select("user_id").From("orders").Where("status = ?", "completed")
db.Select().From("users").Where(relica.In("id", sub)).All(&users)

// Find users with at least one order (EXISTS is often faster)
orderCheck := db.Select("1").From("orders").Where("orders.user_id = users.id")
db.Select().From("users").Where(relica.Exists(orderCheck)).All(&users)

FROM Subqueries:

// Calculate aggregates, then filter
stats := db.Select("user_id", "COUNT(*) as order_count", "SUM(total) as total_spent").
    From("orders").
    GroupBy("user_id")

db.Select("user_id", "order_count", "total_spent").
    FromSelect(stats, "order_stats").
    Where("order_count > ? AND total_spent > ?", 10, 5000).
    All(&topCustomers)

Scalar Subqueries in SELECT (type-safe, no raw SQL):

// Count orders per user — correlated subquery
sub := db.Select("COUNT(*)").From("orders").
    Where(relica.EqCol("orders.user_id", "users.id"))
db.Select("id", "name").
    SelectSub(sub.AsExpression(), "order_count").
    From("users").All(&users)

See Subquery Guide for complete examples and performance tips.

Set Operations

UNION/UNION ALL:

// Combine active and archived users (UNION removes duplicates)
active := db.Select("name").From("users").Where("status = ?", 1)
archived := db.Select("name").From("archived_users").Where("status = ?", 1)
active.Union(archived).All(&allNames)

// UNION ALL is 2-3x faster (keeps duplicates)
active.UnionAll(archived).All(&allNames)

INTERSECT/EXCEPT (PostgreSQL, MySQL 8.0.31+, SQLite):

// Find users who have placed orders (INTERSECT)
allUsers := db.Select("id").From("users")
orderUsers := db.Select("user_id").From("orders")
allUsers.Intersect(orderUsers).All(&activeUsers)

// Find users without orders (EXCEPT)
allUsers.Except(orderUsers).All(&inactiveUsers)

See Set Operations Guide for database compatibility and workarounds.

Common Table Expressions (CTEs)

Basic CTEs:

// Define reusable query
orderTotals := db.Select("user_id", "SUM(total) as total").
    From("orders").
    GroupBy("user_id")

// Use CTE in main query
db.Select().
    With("order_totals", orderTotals).
    From("order_totals").
    Where("total > ?", 1000).
    All(&premiumUsers)

Recursive CTEs (organizational hierarchies, trees):

// Anchor: top-level employees
anchor := db.Select("id", "name", "manager_id", "1 as level").
    From("employees").
    Where("manager_id IS NULL")

// Recursive: children
recursive := db.Select("e.id", "e.name", "e.manager_id", "h.level + 1").
    From("employees e").
    InnerJoin("hierarchy h", "e.manager_id = h.id")

// Build hierarchy
db.Select().
    WithRecursive("hierarchy", anchor.UnionAll(recursive)).
    From("hierarchy").
    OrderBy("level", "name").
    All(&orgChart)

See CTE Guide for hierarchical data examples (org charts, bill of materials, category trees).

Window Functions

Relica supports window functions via SelectExpr() for advanced analytics:

// Rank users by order total within each country
db.Select().
    SelectExpr("user_id", "country", "total",
        "RANK() OVER (PARTITION BY country ORDER BY total DESC) as rank").
    From("orders").
    All(&rankedOrders)

// Running totals with frame specification
db.Select().
    SelectExpr("date", "amount",
        "SUM(amount) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as running_total").
    From("transactions").
    OrderBy("date").
    All(&runningTotals)

See Window Functions Guide for complete reference with RANK(), ROW_NUMBER(), LAG(), LEAD(), and frame specifications.

Transactions
// Start transaction
tx, err := db.BeginTx(ctx, &relica.TxOptions{
    Isolation: sql.LevelSerializable,
})
if err != nil {
    return err
}
defer tx.Rollback() // Rollback if not committed

// Execute queries within transaction
_, err = tx.Insert("users", userData).Execute()
if err != nil {
    return err
}

_, err = tx.Update("accounts").
    Set(map[string]interface{}{"balance": newBalance}).
    Where("user_id = ?", userID).
    Execute()
if err != nil {
    return err
}

// Commit transaction
return tx.Commit()
Batch Operations

Batch INSERT (3.3x faster than individual inserts):

result, err := db.BatchInsert("users", []string{"name", "email"}).
    Values("Alice", "alice@example.com").
    Values("Bob", "bob@example.com").
    Values("Charlie", "charlie@example.com").
    Execute()

// Or from a slice
users := []User{
    {Name: "Alice", Email: "alice@example.com"},
    {Name: "Bob", Email: "bob@example.com"},
}

batch := db.BatchInsert("users", []string{"name", "email"})
for _, user := range users {
    batch.Values(user.Name, user.Email)
}
result, err := batch.Execute()

Batch UPDATE (updates multiple rows with different values):

result, err := db.BatchUpdate("users", "id").
    Set(1, map[string]interface{}{"name": "Alice Updated", "status": "active"}).
    Set(2, map[string]interface{}{"name": "Bob Updated", "status": "active"}).
    Set(3, map[string]interface{}{"age": 30}).
    Execute()
Context Support
// Query with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var users []User
err := db.Select().
    From("users").
    WithContext(ctx).
    All(&users)

// Context on query level
err = db.Select().
    From("users").
    WithContext(ctx).
    One(&user)

// Transaction context auto-propagates
tx, _ := db.BeginTx(ctx, nil)
tx.Select().From("users").One(&user) // Uses ctx automatically

🏗️ Database Support

Database Status Placeholders Identifiers UPSERT
PostgreSQL ✅ Full $1, $2, $3 "users" ON CONFLICT
MySQL ✅ Full ?, ?, ? `users` ON DUPLICATE KEY
SQLite ✅ Full ?, ?, ? "users" ON CONFLICT

⚡ Performance

Statement Cache
  • Default capacity: 1000 prepared statements
  • Hit latency: <60ns
  • Thread-safe: Concurrent access optimized
  • Metrics: Hit rate, evictions, cache size
// Configure cache capacity
db, err := relica.Open("postgres", dsn,
    relica.WithStmtCacheCapacity(2000),
    relica.WithMaxOpenConns(25),
    relica.WithMaxIdleConns(5),
)

// Check connection pool statistics
stats := db.Stats()
fmt.Printf("Open: %d, Idle: %d, InUse: %d\n",
    stats.OpenConnections, stats.Idle, stats.InUse)
Batch Operations Performance
Operation Rows Time vs Single Memory
Batch INSERT 100 327ms 3.3x faster -15%
Single INSERT 100 1094ms Baseline Baseline
Batch UPDATE 100 1370ms 2.5x faster -55% allocs

🔧 Configuration

db, err := relica.Open("postgres", dsn,
    // Connection pool
    relica.WithMaxOpenConns(25),
    relica.WithMaxIdleConns(5),

    // Statement cache
    relica.WithStmtCacheCapacity(1000),
)
Connection Management
Standard Connection
// Create new connection with Relica managing the pool
db, err := relica.Open("postgres", dsn)
defer db.Close()
Wrap Existing Connection

Use WrapDB() when you need to integrate Relica with an existing *sql.DB connection:

import (
    "database/sql"
    "time"

    "github.com/coregx/relica"
    _ "github.com/lib/pq"
)

// Create and configure external connection pool
sqlDB, err := sql.Open("postgres", dsn)
if err != nil {
    log.Fatal(err)
}

// Apply custom pool settings
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(50)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)

// Wrap with Relica query builder
db := relica.WrapDB(sqlDB, "postgres")

// Use Relica's fluent API
var users []User
err = db.Select().
    From("users").
    Where("status = ?", 1).
    All(&users)

// Caller is responsible for closing the connection
defer sqlDB.Close()  // NOT db.Close()

Use Cases for WrapDB:

  • Existing Codebase Integration: Add Relica to projects with established *sql.DB connections
  • Custom Pool Configuration: Apply advanced connection pool settings before wrapping
  • Shared Connections: Multiple parts of your application can share the same pool
  • Testing: Wrap test database connections without managing lifecycle

Important Notes:

  • Each WrapDB() call creates a new Relica instance with its own statement cache
  • The caller is responsible for closing the underlying *sql.DB connection
  • Multiple wraps of the same connection are isolated (separate caches)

🛡️ Enterprise Security

Relica provides enterprise-grade security features for protecting your database operations:

SQL Injection Prevention

Pattern-based detection of OWASP Top 10 SQL injection attacks with <2% overhead.

Note: Security features (WithValidator, WithAuditLog) use internal types from internal/security. See Security Guide for integration instructions.

Relica's primary defense against SQL injection is the use of parameterized queries (placeholders ?). All query builder methods pass values as parameters, never interpolated into SQL strings:

// Safe - values are always parameterized
db.Select().From("users").
    Where(relica.Eq("id", userInput)).
    One(&user)

// Safe - Model() API uses parameterized queries internally
db.Model(&user).Insert()
db.Model(&user).Update("status")

Detected attack vectors:

  • Tautology attacks (1 OR 1=1)
  • Comment injection (admin'--)
  • Stacked queries (; DROP TABLE)
  • UNION attacks
  • Command execution (xp_cmdshell, exec())
  • Information schema access
  • Timing attacks (pg_sleep, benchmark)
Audit Logging

Comprehensive operation tracking for GDPR, HIPAA, PCI-DSS, SOC2 compliance.

Note: Audit logging uses internal types. See Security Guide for integration instructions.

Audit log includes:

  • Timestamp, user, client IP, request ID
  • Operation (INSERT, UPDATE, DELETE, SELECT)
  • Query execution time
  • Success/failure status
  • Parameter hashing (NOT raw values) for GDPR compliance
Security Guides

📖 Documentation

Migration Guides

Switching from another library? We've got you covered:

  • Migration from GORM - Complete guide for GORM users

    • ORM vs Query Builder philosophy
    • Side-by-side API comparisons
    • Association handling (Preload → JOIN)
    • Gradual migration strategies
  • Migration from sqlx - Complete guide for sqlx users

    • Drop-in replacement patterns
    • Query builder advantages
    • Statement caching benefits
    • Using both together
Comprehensive User Guides

Getting Started:

Production:

Advanced:

SQL Feature Guides
Additional Resources

🧪 Testing

# Run unit tests
go test ./...

# Run with coverage
go test -cover ./...

# Run integration tests (requires Docker)
go test -tags=integration ./test/...

# Run benchmarks
go test -bench=. -benchmem ./benchmark/...

🎯 Design Philosophy

  1. Zero Dependencies - Production code uses only Go standard library
  2. Type Safety - Compile-time checks, runtime safety
  3. Performance - Statement caching, batch operations, zero allocations in hot paths
  4. Simplicity - Clean API, easy to learn, hard to misuse
  5. Correctness - ACID transactions, proper error handling
  6. Observability - Built-in metrics, context support for tracing

📊 Project Status

  • Go Version: 1.25+
  • Production Ready: Yes (beta)
  • Test Coverage: 88.2%
  • Dependencies: 0 (production), test-only: testify + DB drivers
  • API: Stable public API, internal packages protected

🤝 Contributing

Contributions are welcome! Please read our Contributing Guide first.

📝 License

Relica is released under the MIT License.

🙏 Acknowledgments

  • Inspired by ozzo-dbx
  • Built with Go 1.25+ features
  • Zero-dependency philosophy inspired by Go standard library

📞 Support

✨ Special Thanks

Professor Ancha Baranova - This project would not have been possible without her invaluable help and support. Her assistance was crucial in bringing Relica to life.


Made with ❤️ by Andrey Kolkov and CoreGX contributors

Relica - Lightweight, Fast, Zero-Dependency Database Query Builder for Go

Documentation

Overview

Package relica provides a lightweight, type-safe database query builder for Go.

Relica offers a fluent API for building SQL queries with support for:

  • Multiple databases (PostgreSQL, MySQL, SQLite)
  • Zero production dependencies
  • Prepared statement caching
  • Transaction management
  • Advanced SQL features (JOINs, aggregates, subqueries, CTEs)

Quick Start

Install:

go get github.com/coregx/relica

Basic usage:

db, err := relica.Open("postgres", "user=postgres dbname=myapp")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

var users []User
err = db.Select("id", "name").From("users").All(&users)

Features

CRUD Operations:

// SELECT with Expression API
db.Select().From("users").Where(relica.Eq("id", 123)).One(&user)

// INSERT via Model API (recommended)
user := User{Name: "Alice", Email: "alice@example.com"}
db.Model(&user).Insert()

// UPDATE
db.Update("users").
    Set(map[string]interface{}{"status": "active"}).
    Where(relica.Eq("id", 123)).
    Execute()

// DELETE
db.Delete("users").Where(relica.Eq("id", 123)).Execute()

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = core.ErrNotFound

ErrNotFound is returned by One() when no rows match the query. It wraps sql.ErrNoRows so both errors.Is(err, relica.ErrNotFound) and errors.Is(err, sql.ErrNoRows) return true.

Example:

var user User
err := db.Select().From("users").Where(relica.Eq("id", 999)).One(&user)
if errors.Is(err, relica.ErrNotFound) {
    // handle not found
}

Functions

func DetectOperation added in v0.9.0

func DetectOperation(query string) string

DetectOperation detects the SQL operation type (SELECT, INSERT, UPDATE, DELETE, UNKNOWN).

func IsCheckViolation added in v0.11.0

func IsCheckViolation(err error) bool

IsCheckViolation reports whether err represents a CHECK constraint violation. Works with PostgreSQL, MySQL, and SQLite. Returns false for nil errors.

Example:

_, err := db.Model(&product).Insert()
if relica.IsCheckViolation(err) {
    // handle check constraint failure
}

func IsForeignKeyViolation added in v0.11.0

func IsForeignKeyViolation(err error) bool

IsForeignKeyViolation reports whether err represents a foreign key constraint violation. Works with PostgreSQL, MySQL, and SQLite. Returns false for nil errors.

Example:

_, err := db.Model(&order).Insert()
if relica.IsForeignKeyViolation(err) {
    // handle missing referenced row
}

func IsNotNullViolation added in v0.11.0

func IsNotNullViolation(err error) bool

IsNotNullViolation reports whether err represents a NOT NULL constraint violation. Works with PostgreSQL, MySQL, and SQLite. Returns false for nil errors.

Example:

_, err := db.Model(&user).Insert()
if relica.IsNotNullViolation(err) {
    // handle missing required field
}

func IsUniqueViolation added in v0.11.0

func IsUniqueViolation(err error) bool

IsUniqueViolation reports whether err represents a unique constraint violation. Works with PostgreSQL, MySQL, and SQLite. Returns false for nil errors.

Example:

_, err := db.Model(&user).Insert()
if relica.IsUniqueViolation(err) {
    // handle duplicate key
}

Types

type BatchInsertQuery

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

BatchInsertQuery represents a batch INSERT query being built.

func (*BatchInsertQuery) Build

func (biq *BatchInsertQuery) Build() *Query

Build constructs the Query object.

func (*BatchInsertQuery) Execute

func (biq *BatchInsertQuery) Execute() (sql.Result, error)

Execute executes the batch INSERT query.

func (*BatchInsertQuery) ToSQL added in v0.12.0

func (biq *BatchInsertQuery) ToSQL() (string, []interface{})

ToSQL returns the SQL string and parameters without executing the query. This is useful for debugging, logging, or passing the query to another layer.

Example:

sql, params := db.BatchInsert("users", []string{"name", "email"}).
    Values("Alice", "alice@example.com").ToSQL()

func (*BatchInsertQuery) Values

func (biq *BatchInsertQuery) Values(values ...interface{}) *BatchInsertQuery

Values adds a row of values to the batch insert.

Example:

BatchInsert("users", []string{"name", "email"}).
    Values("Alice", "alice@example.com").
    Values("Bob", "bob@example.com")

func (*BatchInsertQuery) ValuesMap

func (biq *BatchInsertQuery) ValuesMap(values map[string]interface{}) *BatchInsertQuery

ValuesMap adds a row from a map.

Example:

BatchInsert("users", []string{"name", "email"}).
    ValuesMap(map[string]interface{}{"name": "Alice", "email": "alice@example.com"})

func (*BatchInsertQuery) WithContext

func (biq *BatchInsertQuery) WithContext(ctx context.Context) *BatchInsertQuery

WithContext sets the context for this batch INSERT query.

type BatchUpdateQuery

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

BatchUpdateQuery represents a batch UPDATE query being built.

func (*BatchUpdateQuery) Build

func (buq *BatchUpdateQuery) Build() *Query

Build constructs the Query object.

func (*BatchUpdateQuery) Execute

func (buq *BatchUpdateQuery) Execute() (sql.Result, error)

Execute executes the batch UPDATE query.

func (*BatchUpdateQuery) Set

func (buq *BatchUpdateQuery) Set(keyValue interface{}, values map[string]interface{}) *BatchUpdateQuery

Set adds a row update to the batch.

Example:

BatchUpdate("users", "id").
    Set(1, map[string]interface{}{"status": 2}).
    Set(2, map[string]interface{}{"status": 3})

func (*BatchUpdateQuery) ToSQL added in v0.12.0

func (buq *BatchUpdateQuery) ToSQL() (string, []interface{})

ToSQL returns the SQL string and parameters without executing the query. This is useful for debugging, logging, or passing the query to another layer.

Example:

sql, params := db.BatchUpdate("users", "id").
    Set(1, map[string]interface{}{"status": 2}).ToSQL()

func (*BatchUpdateQuery) WithContext

func (buq *BatchUpdateQuery) WithContext(ctx context.Context) *BatchUpdateQuery

WithContext sets the context for this batch UPDATE query.

type CaseExp added in v0.9.0

type CaseExp = core.CaseExp

CaseExp represents a SQL CASE expression.

func Case added in v0.9.0

func Case(column string) *CaseExp

Case creates a simple CASE expression.

func CaseWhen added in v0.9.0

func CaseWhen() *CaseExp

CaseWhen creates a searched CASE expression (without column).

type CoalesceExp added in v0.9.0

type CoalesceExp = core.CoalesceExp

CoalesceExp represents a SQL COALESCE expression.

func Coalesce added in v0.9.0

func Coalesce(values ...interface{}) *CoalesceExp

Coalesce creates a COALESCE expression.

type ConcatExp added in v0.9.0

type ConcatExp = core.ConcatExp

ConcatExp represents a SQL string concatenation expression.

func Concat added in v0.9.0

func Concat(values ...interface{}) *ConcatExp

Concat creates a string concatenation expression.

type DB

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

DB represents a database connection with query building capabilities.

DB provides a fluent API for constructing and executing SQL queries in a type-safe manner. It wraps the underlying database/sql connection and adds features like:

  • Prepared statement caching (LRU eviction, <60ns hit latency)
  • Query builder with method chaining
  • Transaction management (all isolation levels)
  • Multi-database support (PostgreSQL, MySQL, SQLite)

Example:

db, err := relica.Open("postgres", "user=postgres dbname=myapp")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

var users []User
err = db.Builder().
    Select("id", "name", "email").
    From("users").
    Where("active = ?", true).
    OrderBy("name").
    All(&users)

func NewDB deprecated

func NewDB(driverName, dsn string) (*DB, error)

NewDB creates a database connection.

Deprecated: Use Open instead. NewDB will be removed in a future version.

Example:

db, err := relica.NewDB("postgres", dsn)

func Open

func Open(driverName, dsn string, opts ...Option) (*DB, error)

Open creates a new database connection with optional configuration.

The driverName parameter specifies the database driver:

  • "postgres" - PostgreSQL
  • "mysql" - MySQL
  • "sqlite3" - SQLite

The dsn parameter is the database-specific connection string.

Example:

db, err := relica.Open("postgres", "user=postgres dbname=myapp",
    relica.WithMaxOpenConns(100),
    relica.WithMaxIdleConns(50))
if err != nil {
    log.Fatal(err)
}
defer db.Close()

func WrapDB

func WrapDB(sqlDB *sql.DB, driverName string) *DB

WrapDB wraps an existing *sql.DB connection with Relica's query builder.

The caller is responsible for managing the connection lifecycle (including Close()). This is useful when you need to:

  • Use Relica with an externally managed connection pool
  • Integrate with existing code that already has a *sql.DB instance
  • Apply custom connection pool settings before wrapping

Example:

sqlDB, _ := sql.Open("postgres", dsn)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
db := relica.WrapDB(sqlDB, "postgres")
defer sqlDB.Close() // Caller's responsibility

func (*DB) BatchInsert added in v0.10.0

func (d *DB) BatchInsert(table string, columns []string) *BatchInsertQuery

BatchInsert creates a new batch INSERT query for inserting multiple rows efficiently.

This is a convenience method equivalent to db.Builder().BatchInsert(table, columns).

Example:

result, err := db.BatchInsert("users", []string{"name", "email"}).
    Values("Alice", "alice@example.com").
    Values("Bob", "bob@example.com").
    Execute()

func (*DB) BatchInsertStruct added in v0.6.0

func (d *DB) BatchInsertStruct(table string, data interface{}) *Query

BatchInsertStruct builds a batch INSERT query from a slice of structs.

This method performs batch insertion of multiple structs in a single query, which is significantly faster than individual inserts. All structs must have the same type and will be inserted with the same column set.

The slice element type must be a struct or pointer to struct. Fields are mapped using the same rules as InsertStruct.

Example:

users := []User{
    {Name: "Alice", Email: "alice@example.com"},
    {Name: "Bob", Email: "bob@example.com"},
}
result, err := db.BatchInsertStruct("users", users).Execute()
if err != nil {
    return err
}

For single struct inserts, use InsertStruct instead.

func (*DB) BatchUpdate added in v0.10.0

func (d *DB) BatchUpdate(table, keyColumn string) *BatchUpdateQuery

BatchUpdate creates a new batch UPDATE query for updating multiple rows with different values.

This is a convenience method equivalent to db.Builder().BatchUpdate(table, keyColumn).

Example:

result, err := db.BatchUpdate("users", "id").
    Set(1, map[string]interface{}{"name": "Alice"}).
    Set(2, map[string]interface{}{"name": "Bob"}).
    Execute()

func (*DB) Begin

func (d *DB) Begin(ctx context.Context) (*Tx, error)

Begin starts a transaction with default options.

The transaction must be committed or rolled back to release resources. It's safe to call Rollback() even after Commit().

Example:

tx, err := db.Begin(ctx)
if err != nil {
    return err
}
defer tx.Rollback() // Safe even after Commit

// Use transaction
_, err = tx.Insert("users", data).Execute()
if err != nil {
    return err
}

return tx.Commit()

func (*DB) BeginTx

func (d *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)

BeginTx starts a transaction with specified options.

Options can specify isolation level and read-only mode:

  • Isolation: sql.LevelReadUncommitted, sql.LevelReadCommitted, sql.LevelRepeatableRead, sql.LevelSerializable
  • ReadOnly: true for read-only transactions (some databases optimize these)

Example:

opts := &relica.TxOptions{
    Isolation: sql.LevelSerializable,
    ReadOnly:  false,
}
tx, err := db.BeginTx(ctx, opts)

func (*DB) Builder

func (d *DB) Builder() *QueryBuilder

Builder returns a new QueryBuilder for constructing queries.

The query builder provides a fluent interface for building SELECT, INSERT, UPDATE, DELETE, and UPSERT queries.

Example:

db.Builder().
    Select("*").
    From("users").
    Where("id = ?", 123).
    One(&user)

func (*DB) Close

func (d *DB) Close() error

Close releases all database resources including the connection pool and statement cache.

After calling Close, the DB instance should not be used.

Example:

db, _ := relica.Open("postgres", dsn)
defer db.Close()

func (*DB) Delete

func (d *DB) Delete(table string) *DeleteQuery

Delete creates a new DELETE query.

This is a convenience method equivalent to db.Builder().Delete(table).

Example:

_, err := db.Delete("users").
    Where("id = ?", 123).
    Execute()
if err != nil {
    return err
}

// Delete multiple rows
_, err := db.Delete("users").
    Where("status = ?", "inactive").
    Execute()

func (*DB) ExecContext

func (d *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)

ExecContext executes a raw SQL query (INSERT/UPDATE/DELETE).

This bypasses the query builder and executes SQL directly. Use this for queries that aren't supported by the query builder or when you need maximum control.

Example:

result, err := db.ExecContext(ctx,
    "UPDATE users SET status = ? WHERE id = ?",
    1, 123)
if err != nil {
    return err
}
rowsAffected, _ := result.RowsAffected()

func (*DB) GenerateParamName

func (d *DB) GenerateParamName(index int) string

GenerateParamName generates a dialect-specific parameter placeholder for the given index. For PostgreSQL, returns "$1", "$2", etc. For MySQL/SQLite, returns "?".

This is useful when building dynamic SQL queries where you need the correct placeholder syntax for the active database driver.

Example:

ph1 := db.GenerateParamName(1) // PostgreSQL: "$1", MySQL/SQLite: "?"
ph2 := db.GenerateParamName(2) // PostgreSQL: "$2", MySQL/SQLite: "?"

func (*DB) Insert

func (d *DB) Insert(table string, data map[string]interface{}) *Query

Insert creates a new INSERT query.

This is a convenience method equivalent to db.Builder().Insert(table, data). For batch inserts, use db.Builder().BatchInsert().

Example:

result, err := db.Insert("users", map[string]interface{}{
    "name":  "Alice",
    "email": "alice@example.com",
}).Execute()
if err != nil {
    return err
}
rows, _ := result.RowsAffected()
fmt.Printf("Inserted %d row(s)\n", rows)

// For batch operations, use Builder()
result, err := db.Builder().
    BatchInsert("users", []string{"name", "email"}).
    Values("Alice", "alice@example.com").
    Values("Bob", "bob@example.com").
    Execute()

func (*DB) InsertStruct added in v0.6.0

func (d *DB) InsertStruct(table string, data interface{}) *Query

InsertStruct builds an INSERT query from a struct using db tags.

The struct fields are mapped to database columns using the `db` struct tag. Fields without a `db` tag use the field name. Fields tagged with `db:"-"` are ignored. Unexported fields are automatically skipped.

This method provides type-safe struct-based inserts without manually constructing maps. For batch struct inserts, use BatchInsertStruct.

Example:

type User struct {
    ID    int    `db:"id"`
    Name  string `db:"name"`
    Email string `db:"email"`
    Skip  int    `db:"-"`  // ignored
}

user := User{Name: "Alice", Email: "alice@example.com"}
result, err := db.InsertStruct("users", &user).Execute()
if err != nil {
    return err
}

func (*DB) IsHealthy

func (d *DB) IsHealthy() bool

IsHealthy returns true if the database connection is healthy. Always returns true if health checks are disabled.

This is a convenience method that calls Stats() internally.

Example:

if !db.IsHealthy() {
    log.Error("Database connection unhealthy")
    // Attempt reconnection or alert
}

func (*DB) Model added in v0.6.0

func (d *DB) Model(model interface{}) *ModelQuery

Model creates a ModelQuery for performing CRUD operations on a struct model.

The model must be a pointer to a struct. The table name and primary key are automatically inferred from the struct definition.

Table name resolution:

  1. If model implements TableName() string, use that value
  2. Otherwise, use struct name lowercased + 's' (e.g., User → users)

Primary key detection:

  1. Field with db:"id" tag
  2. Field with db:"*_id" tag (e.g., db:"user_id")
  3. Field named "ID"

Example:

type User struct {
    ID    int    `db:"id"`
    Name  string `db:"name"`
    Email string `db:"email"`
}

func (User) TableName() string { return "users" }

// INSERT - auto table name.
user := User{Name: "Alice", Email: "alice@example.com"}
err := db.Model(&user).Insert()

// UPDATE - auto WHERE by primary key.
user.Status = "active"
err = db.Model(&user).Update()

// DELETE - auto WHERE by primary key.
err = db.Model(&user).Delete()

// Field control.
err = db.Model(&user).Exclude("CreatedAt", "UpdatedAt").Insert()
err = db.Model(&user).Table("users_archive").Insert()

func (*DB) NewQuery added in v0.9.0

func (d *DB) NewQuery(query string) *Query

NewQuery creates a raw SQL query for execution. Use this for queries that don't fit the query builder pattern, or when you need manual control over prepared statement lifecycle.

Example:

var count int
err := db.NewQuery("SELECT COUNT(*) FROM users").Row(&count)

// With parameters
var user User
err := db.NewQuery("SELECT * FROM users WHERE id = ?").Bind(1).One(&user)

// With Prepare for repeated execution
q := db.NewQuery("SELECT * FROM users WHERE status = ?").Prepare()
defer q.Close()
for _, status := range statuses {
    q.Bind(status).All(&users)
}

func (*DB) PinQuery

func (d *DB) PinQuery(query string) bool

PinQuery marks a query as pinned in the statement cache, preventing eviction.

Pinned queries remain in cache indefinitely, useful for frequently-used queries. Returns false if the query is not in cache (call WarmCache first).

Example:

// Warm and pin critical queries
queries := []string{"SELECT * FROM users WHERE id = ?"}
db.WarmCache(queries)
db.PinQuery(queries[0])  // Will never be evicted

func (*DB) QueryContext

func (d *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)

QueryContext executes a raw SQL query and returns rows.

This bypasses the query builder and executes SQL directly. You are responsible for closing the returned rows.

Example:

rows, err := db.QueryContext(ctx,
    "SELECT * FROM users WHERE status = ?", 1)
if err != nil {
    return err
}
defer rows.Close()

for rows.Next() {
    // Process rows
}

func (*DB) QueryRowContext

func (d *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row

QueryRowContext executes a raw SQL query expected to return at most one row.

This bypasses the query builder and executes SQL directly.

Example:

var count int
err := db.QueryRowContext(ctx,
    "SELECT COUNT(*) FROM users").Scan(&count)

func (*DB) QuoteColumnName

func (d *DB) QuoteColumnName(column string) string

QuoteColumnName quotes a column name using the database's identifier quoting style.

This is useful when building dynamic SQL queries.

Example:

quoted := db.QuoteColumnName("user_id")
// PostgreSQL: "user_id"
// MySQL: `user_id`

func (*DB) QuoteTableName

func (d *DB) QuoteTableName(table string) string

QuoteTableName quotes a table name using the database's identifier quoting style.

This is useful when building dynamic SQL queries.

Example:

quoted := db.QuoteTableName("users")
// PostgreSQL: "users"
// MySQL: `users`

func (*DB) Select

func (d *DB) Select(cols ...string) *SelectQuery

Select creates a new SELECT query.

This is a convenience method equivalent to db.Builder().Select(cols...). For advanced queries (CTEs, subqueries, UNION), use db.Builder() directly.

Example:

var users []User
err := db.Select("id", "name", "email").
    From("users").
    Where("active = ?", true).
    OrderBy("name").
    All(&users)

// For wildcard selection
err := db.Select("*").From("users").All(&users)

// For advanced features, use Builder()
err := db.Builder().
    With("stats", statsQuery).
    Select("*").
    From("stats").
    All(&results)

func (*DB) Stats

func (d *DB) Stats() PoolStats

Stats returns database connection pool statistics.

Stats provides insights into connection pool usage including:

  • Number of open/idle/in-use connections
  • Wait count and duration
  • Connections closed due to max lifetime/idle time
  • Health check status (if enabled)

Example:

stats := db.Stats()
fmt.Printf("Open: %d, Idle: %d, InUse: %d\n",
    stats.OpenConnections, stats.Idle, stats.InUse)
if !stats.Healthy {
    log.Warn("Database health check failed")
}

func (*DB) Transactional added in v0.8.0

func (d *DB) Transactional(ctx context.Context, f func(*Tx) error) error

Transactional executes f within a transaction with automatic commit/rollback.

If f returns an error, the transaction is rolled back and the error is returned. If f panics, the transaction is rolled back and the panic is re-raised. If f completes successfully, the transaction is committed.

This helper simplifies transaction management and ensures proper cleanup in all code paths, including panics.

Example:

err := db.Transactional(ctx, func(tx *relica.Tx) error {
    user := User{Name: "Alice", Email: "alice@example.com"}
    if err := tx.Model(&user).Insert(); err != nil {
        return err  // Auto rollback
    }

    account := Account{UserID: user.ID, Balance: 100}
    if err := tx.Model(&account).Insert(); err != nil {
        return err  // Auto rollback
    }

    return nil  // Auto commit
})

func (*DB) TransactionalTx added in v0.8.0

func (d *DB) TransactionalTx(ctx context.Context, opts *TxOptions, f func(*Tx) error) error

TransactionalTx executes f within a transaction with custom options.

Options can specify isolation level and read-only mode. If f returns an error, the transaction is rolled back and the error is returned. If f panics, the transaction is rolled back and the panic is re-raised. If f completes successfully, the transaction is committed.

Example:

opts := &relica.TxOptions{
    Isolation: sql.LevelSerializable,
    ReadOnly:  false,
}
err := db.TransactionalTx(ctx, opts, func(tx *relica.Tx) error {
    // Perform operations within serializable transaction.
    return tx.Model(&user).Update()
})

func (*DB) UnpinQuery

func (d *DB) UnpinQuery(query string) bool

UnpinQuery removes the pin from a cached query, allowing normal LRU eviction.

Returns false if the query is not in cache or not pinned.

Example:

db.UnpinQuery("SELECT * FROM users WHERE id = ?")

func (*DB) Unwrap

func (d *DB) Unwrap() *core.DB

Unwrap returns the underlying core.DB for advanced use cases.

This method is provided for edge cases where direct access to internal types is needed. Most users should not need this.

Example:

coreDB := db.Unwrap()
// Use coreDB for advanced operations

func (*DB) Update

func (d *DB) Update(table string) *UpdateQuery

Update creates a new UPDATE query.

This is a convenience method equivalent to db.Builder().Update(table). For batch updates, use db.Builder().BatchUpdate().

Example:

_, err := db.Update("users").
    Set(map[string]interface{}{"status": "active"}).
    Where("id = ?", 123).
    Execute()
if err != nil {
    return err
}

// For batch operations, use Builder()
_, err := db.Builder().
    BatchUpdate("users", "id").
    Set(1, map[string]interface{}{"status": "active"}).
    Set(2, map[string]interface{}{"status": "inactive"}).
    Execute()

func (*DB) UpdateStruct added in v0.6.0

func (d *DB) UpdateStruct(table string, data interface{}) *UpdateQuery

UpdateStruct builds an UPDATE query from a struct using db tags.

Similar to InsertStruct, but for UPDATE operations. The struct fields are converted to SET clauses. You must chain a Where() call to specify which rows to update.

This method provides type-safe struct-based updates without manually constructing maps.

Example:

user := User{Name: "Alice Updated", Status: "active"}
result, err := db.UpdateStruct("users", &user).
    Where("id = ?", user.ID).
    Execute()
if err != nil {
    return err
}

For automatic WHERE clause based on primary key, consider using the Model() API (available in v0.6.0+).

func (*DB) Upsert added in v0.10.0

func (d *DB) Upsert(table string, values map[string]interface{}) *UpsertQuery

Upsert creates a new UPSERT query (INSERT ... ON CONFLICT).

This is a convenience method equivalent to db.Builder().Upsert(table, values).

Example:

result, err := db.Upsert("users", map[string]interface{}{
    "email": "alice@example.com",
    "name":  "Alice",
}).OnConflict("email").DoUpdate("name").Execute()

func (*DB) WarmCache

func (d *DB) WarmCache(queries []string) (int, error)

WarmCache pre-warms the statement cache by preparing frequently-used queries.

This improves performance at startup by avoiding cache misses for common queries. The queries are prepared synchronously in the order provided. Returns the number of successfully prepared queries and any error encountered.

Example:

n, err := db.WarmCache([]string{
    "SELECT * FROM users WHERE id = ?",
    "INSERT INTO logs (message, level) VALUES (?, ?)",
    "UPDATE users SET last_login = ? WHERE id = ?",
})
if err != nil {
    log.Warn("Failed to warm cache", "error", err, "warmed", n)
}

func (*DB) WithContext

func (d *DB) WithContext(ctx context.Context) *DB

WithContext returns a new DB with the given context.

The context will be used for all subsequent query operations unless overridden at the query level.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
db := db.WithContext(ctx)
db.Builder().Select("*").From("users").All(&users)

type DeleteQuery

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

DeleteQuery represents a DELETE query being built.

func (*DeleteQuery) AndWhere added in v0.8.0

func (dq *DeleteQuery) AndWhere(condition interface{}, params ...interface{}) *DeleteQuery

AndWhere adds a WHERE condition with AND logic. If no existing WHERE clause exists, behaves like Where().

Example:

db.Builder().Delete("users").
    Where("status = ?", 0).
    AndWhere("created_at < ?", "2020-01-01")

func (*DeleteQuery) Build

func (dq *DeleteQuery) Build() *Query

Build constructs the Query object.

func (*DeleteQuery) Execute

func (dq *DeleteQuery) Execute() (sql.Result, error)

Execute executes the DELETE query.

func (*DeleteQuery) OrWhere added in v0.8.0

func (dq *DeleteQuery) OrWhere(condition interface{}, params ...interface{}) *DeleteQuery

OrWhere adds a WHERE condition with OR logic. If no existing WHERE clause exists, behaves like Where().

Example:

db.Builder().Delete("users").
    Where("banned = ?", true).
    OrWhere("deleted = ?", true)

func (*DeleteQuery) ToSQL added in v0.11.0

func (dq *DeleteQuery) ToSQL() (string, []interface{})

ToSQL returns the SQL string and parameters without executing the query. This is useful for debugging, logging, or passing the query to another layer.

Example:

sql, params := db.Delete("users").Where(relica.Eq("id", 1)).ToSQL()

func (*DeleteQuery) Where

func (dq *DeleteQuery) Where(condition interface{}, params ...interface{}) *DeleteQuery

Where adds a WHERE condition to the DELETE query.

Example:

Delete("users").Where("id = ?", 123)

func (*DeleteQuery) WithContext

func (dq *DeleteQuery) WithContext(ctx context.Context) *DeleteQuery

WithContext sets the context for this DELETE query.

type Expression

type Expression = core.Expression

Expression represents a database expression for building complex WHERE clauses.

Expressions provide a type-safe way to construct SQL conditions without writing raw SQL strings. They support nesting and composition.

Example:

expr := relica.And(
    relica.Eq("status", 1),
    relica.Or(
        relica.GreaterThan("age", 18),
        relica.Eq("verified", true),
    ),
)
db.Builder().Select("*").From("users").Where(expr).All(&users)

func And

func And(exps ...Expression) Expression

And combines expressions with AND.

func Between

func Between(col string, from, to interface{}) Expression

Between creates a BETWEEN expression (column BETWEEN low AND high).

func Eq

func Eq(col string, value interface{}) Expression

Eq creates an equality expression (column = value).

func EqCol added in v0.13.0

func EqCol(col1, col2 string) Expression

EqCol creates a column-to-column equality expression (col1 = col2). Both identifiers are quoted using the current dialect. Useful for JOIN ON conditions and correlated subquery WHERE clauses.

Example:

relica.EqCol("orders.user_id", "users.id")  →  "orders"."user_id" = "users"."id"

func Exists

func Exists(exp Expression) Expression

Exists creates an EXISTS subquery expression.

func GreaterOrEqual

func GreaterOrEqual(col string, value interface{}) Expression

GreaterOrEqual creates a greater-or-equal expression (column >= value).

func GreaterThan

func GreaterThan(col string, value interface{}) Expression

GreaterThan creates a greater-than expression (column > value).

func GreaterThanCol added in v0.13.0

func GreaterThanCol(col1, col2 string) Expression

GreaterThanCol creates a column-to-column greater-than expression (col1 > col2).

func In

func In(col string, values ...interface{}) Expression

In creates an IN expression (column IN (values...)).

func LessOrEqual

func LessOrEqual(col string, value interface{}) Expression

LessOrEqual creates a less-or-equal expression (column <= value).

func LessThan

func LessThan(col string, value interface{}) Expression

LessThan creates a less-than expression (column < value).

func LessThanCol added in v0.13.0

func LessThanCol(col1, col2 string) Expression

LessThanCol creates a column-to-column less-than expression (col1 < col2).

func NewExp

func NewExp(rawSQL string, args ...interface{}) Expression

NewExp creates a new raw SQL expression.

func Not

func Not(exp Expression) Expression

Not negates an expression.

func NotBetween

func NotBetween(col string, from, to interface{}) Expression

NotBetween creates a NOT BETWEEN expression.

func NotEq

func NotEq(col string, value interface{}) Expression

NotEq creates a not-equal expression (column != value).

func NotEqCol added in v0.13.0

func NotEqCol(col1, col2 string) Expression

NotEqCol creates a column-to-column inequality expression (col1 <> col2).

func NotExists

func NotExists(exp Expression) Expression

NotExists creates a NOT EXISTS subquery expression.

func NotIn

func NotIn(col string, values ...interface{}) Expression

NotIn creates a NOT IN expression (column NOT IN (values...)).

func Or

func Or(exps ...Expression) Expression

Or combines expressions with OR.

type GreatestLeastExp added in v0.9.0

type GreatestLeastExp = core.GreatestLeastExp

GreatestLeastExp represents a SQL GREATEST or LEAST expression.

func Greatest added in v0.9.0

func Greatest(values ...interface{}) *GreatestLeastExp

Greatest creates a GREATEST expression.

func Least added in v0.9.0

func Least(values ...interface{}) *GreatestLeastExp

Least creates a LEAST expression.

type HashExp

type HashExp = core.HashExp

HashExp represents a hash-based expression using column-value pairs.

HashExp provides a convenient map syntax for simple equality conditions. Special values are handled automatically:

  • nil → "column IS NULL"
  • []interface{} → "column IN (...)"

Example:

db.Builder().Select("*").From("users").Where(relica.HashExp{
    "status": 1,
    "role": []string{"admin", "moderator"},
    "deleted_at": nil,
}).All(&users)

type LikeExp

type LikeExp = core.LikeExp

LikeExp represents a LIKE expression with automatic escaping.

LikeExp provides pattern matching with automatic escaping of SQL wildcard characters (%, _).

Example:

db.Builder().Select("*").From("users").Where(
    relica.Like("name", "john%"),
).All(&users)

func Like

func Like(col string, values ...string) *LikeExp

Like creates a LIKE expression with automatic escaping.

func NotLike

func NotLike(col string, values ...string) *LikeExp

NotLike creates a NOT LIKE expression.

func OrLike

func OrLike(col string, values ...string) *LikeExp

OrLike creates a LIKE expression combined with OR.

func OrNotLike

func OrNotLike(col string, values ...string) *LikeExp

OrNotLike creates a NOT LIKE expression combined with OR.

type Logger

type Logger = logger.Logger

Logger defines the logging interface for Relica. Implementations should handle structured logging with key-value pairs.

type ModelQuery added in v0.6.0

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

ModelQuery provides CRUD operations for struct models.

ModelQuery simplifies database operations by automatically inferring table names and primary keys from struct definitions, similar to ozzo-dbx.

The table name is determined by:

  1. TableName() method if model implements interface{ TableName() string }
  2. Otherwise: struct name lowercased with 's' suffix (e.g., User → users)

The primary key is detected from:

  1. Field with db:"id" tag
  2. Field with db:"*_id" tag (e.g., db:"user_id")
  3. Field named "ID"

Example:

type User struct {
    ID    int    `db:"id"`
    Name  string `db:"name"`
    Email string `db:"email"`
}

func (User) TableName() string { return "users" }

user := User{Name: "Alice", Email: "alice@example.com"}
err := db.Model(&user).Insert()

user.Status = "active"
err = db.Model(&user).Update() // Auto WHERE by primary key.

err = db.Model(&user).Delete() // Auto WHERE by primary key.

func (*ModelQuery) Delete added in v0.6.0

func (mq *ModelQuery) Delete() error

Delete deletes the model from the table.

The WHERE clause is automatically generated using the primary key.

Example:

err := db.Model(&user).Delete()
// DELETE FROM users WHERE id=?

func (*ModelQuery) Exclude added in v0.6.0

func (mq *ModelQuery) Exclude(attrs ...string) *ModelQuery

Exclude excludes the specified fields from the operation.

This is useful for auto-managed fields like timestamps.

Example:

err := db.Model(&user).Exclude("created_at", "updated_at").Insert()

func (*ModelQuery) Insert added in v0.6.0

func (mq *ModelQuery) Insert(attrs ...string) error

Insert inserts the model into the table.

By default, all public fields are inserted. You can specify which fields to insert by passing their names, or use Exclude() to exclude specific fields.

Example:

// Insert all fields.
err := db.Model(&user).Insert()

// Insert only specific fields.
err := db.Model(&user).Insert("name", "email")

// Exclude fields.
err := db.Model(&user).Exclude("created_at").Insert()

func (*ModelQuery) Table added in v0.6.0

func (mq *ModelQuery) Table(name string) *ModelQuery

Table overrides the table name for this operation.

This is useful for partitioned tables or archival operations.

Example:

err := db.Model(&user).Table("users_archive").Insert()

func (*ModelQuery) Update added in v0.6.0

func (mq *ModelQuery) Update(attrs ...string) error

Update updates the model in the table.

The WHERE clause is automatically generated using the primary key. The primary key is detected from:

  1. Field with db:"id" tag
  2. Field with db:"*_id" tag (e.g., db:"user_id")
  3. Field named "ID"

Example:

user.Status = "active"
err := db.Model(&user).Update()
// UPDATE users SET name=?, email=?, status=? WHERE id=?

// Update only specific fields.
err := db.Model(&user).Update("status")
// UPDATE users SET status=? WHERE id=?

func (*ModelQuery) UpdateChanged added in v0.11.0

func (mq *ModelQuery) UpdateChanged(original interface{}) error

UpdateChanged updates only the fields that differ between the current model and the original snapshot.

It compares each field of the current model against original. Only changed fields are included in the UPDATE SET clause. Primary key fields are never included in the SET clause.

If nothing has changed, no query is executed and nil is returned. The original parameter must be the same type as the model struct.

Example:

var user User
db.Select().From("users").Where(relica.Eq("id", 1)).One(&user)

original := user
user.Name = "Alice Updated"
user.Status = "inactive"

err := db.Model(&user).UpdateChanged(&original)
// UPDATE users SET name=?, status=? WHERE id=?

func (*ModelQuery) Upsert added in v0.11.0

func (mq *ModelQuery) Upsert(fields ...string) error

Upsert performs an INSERT ... ON CONFLICT DO UPDATE for the model.

Auto-detects the conflict column from the primary key. If fields are specified, only those fields are updated on conflict. If no fields are specified, all non-PK fields are updated on conflict.

For PostgreSQL, the primary key is auto-populated after upsert (using RETURNING). For MySQL/SQLite, LastInsertId() is used when the PK is zero.

Example:

user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
err := db.Model(&user).Upsert()
// INSERT INTO users (email, id, name) VALUES (?, ?, ?)
// ON CONFLICT (id) DO UPDATE SET email=EXCLUDED.email, name=EXCLUDED.name

// Update only specific fields on conflict:
err = db.Model(&user).Upsert("name")
// ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name

func (*ModelQuery) WithContext added in v0.12.0

func (mq *ModelQuery) WithContext(ctx context.Context) *ModelQuery

WithContext sets the context for this model operation.

The context is propagated to the underlying query execution, enabling cancellation and deadline support.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := db.Model(&user).WithContext(ctx).Insert()

type NoopLogger

type NoopLogger = logger.NoopLogger

NoopLogger is a logger that does nothing (zero overhead when logging is disabled).

type NullIfExp added in v0.9.0

type NullIfExp = core.NullIfExp

NullIfExp represents a SQL NULLIF expression.

func NullIf added in v0.9.0

func NullIf(expr1, expr2 interface{}) *NullIfExp

NullIf creates a NULLIF expression.

type NullStringMap added in v0.9.0

type NullStringMap = core.NullStringMap

NullStringMap represents a map of nullable string values scanned from database rows. Each value is a sql.NullString that can be checked for NULL. This type is useful for dynamic queries where the schema is not known at compile time.

Example:

var result relica.NullStringMap
db.Select("*").From("users").Where("id = ?", 1).One(&result)
name := result.String("name")   // returns empty string if NULL
if !result.IsNull("email") {
    email := result.String("email")
}

// Multiple rows
var results []relica.NullStringMap
db.Select("*").From("users").All(&results)

type Option

type Option = core.Option

Option is a functional option for configuring DB.

Example:

db, err := relica.Open("postgres", dsn,
    relica.WithMaxOpenConns(100),
    relica.WithMaxIdleConns(50))

func WithConnMaxIdleTime

func WithConnMaxIdleTime(d time.Duration) Option

WithConnMaxIdleTime sets the maximum amount of time a connection may be idle. Expired connections may be closed lazily before reuse. If duration <= 0, connections are not closed due to a connection's idle time.

Example:

db, err := relica.Open("postgres", dsn,
    relica.WithConnMaxIdleTime(1*time.Minute))

func WithConnMaxLifetime

func WithConnMaxLifetime(d time.Duration) Option

WithConnMaxLifetime sets the maximum amount of time a connection may be reused. Expired connections may be closed lazily before reuse. If duration <= 0, connections are not closed due to a connection's age.

Example:

db, err := relica.Open("postgres", dsn,
    relica.WithConnMaxLifetime(5*time.Minute))

func WithHealthCheck

func WithHealthCheck(interval time.Duration) Option

WithHealthCheck enables periodic health checks on database connections. The health checker pings the database at the specified interval to detect dead connections. If interval <= 0, health checks are disabled.

Example:

db, err := relica.Open("postgres", dsn,
    relica.WithHealthCheck(30*time.Second))

func WithLogger

func WithLogger(l Logger) Option

WithLogger sets the logger for database query logging. If not set, a NoopLogger is used (zero overhead when logging is disabled).

Example:

import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
db, err := relica.Open("postgres", dsn,
    relica.WithLogger(logger.NewSlogAdapter(logger)))

func WithMaxIdleConns

func WithMaxIdleConns(n int) Option

WithMaxIdleConns sets the maximum number of idle connections.

func WithMaxOpenConns

func WithMaxOpenConns(n int) Option

WithMaxOpenConns sets the maximum number of open connections.

func WithQueryHook added in v0.9.0

func WithQueryHook(hook QueryHook) Option

WithQueryHook sets a callback function that is invoked after each query execution. Use this for logging, metrics, distributed tracing, or debugging. If not set, no hook is called (zero overhead).

Example:

db, _ := relica.Open("postgres", dsn,
    relica.WithQueryHook(func(ctx context.Context, e relica.QueryEvent) {
        slog.Info("query", "sql", e.SQL, "duration", e.Duration, "err", e.Error)
    }))

func WithSensitiveFields

func WithSensitiveFields(fields []string) Option

WithSensitiveFields sets the list of sensitive field names for parameter masking. If not set, default sensitive field patterns are used.

Example:

db, err := relica.Open("postgres", dsn,
    relica.WithSensitiveFields([]string{"password", "token", "api_key"}))

func WithStmtCacheCapacity

func WithStmtCacheCapacity(capacity int) Option

WithStmtCacheCapacity sets the prepared statement cache capacity.

type Params added in v0.9.0

type Params = core.Params

Params represents named parameter values for query binding. Named parameters are specified in SQL using {:name} syntax.

Example:

db.NewQuery("SELECT * FROM users WHERE id={:id} AND status={:status}").
    BindParams(relica.Params{"id": 1, "status": "active"}).
    All(&users)

type PoolStats

type PoolStats = core.PoolStats

PoolStats represents database connection pool statistics. It provides insights into connection pool health and usage patterns.

type Query

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

Query represents a built query ready for execution.

Query encapsulates the SQL string, parameters, and execution context. It provides methods for executing the query and scanning results.

Example:

q := db.Builder().Select("*").From("users").Where("id = ?", 123).Build()
var user User
err := q.One(&user)

func (*Query) All

func (q *Query) All(dest interface{}) error

All fetches all rows into dest slice.

func (*Query) Bind added in v0.9.0

func (q *Query) Bind(params ...interface{}) *Query

Bind sets positional parameters for the query. Parameters replace ? placeholders in order.

Example:

db.NewQuery("SELECT * FROM users WHERE id = ? AND status = ?").
    Bind(1, "active").
    One(&user)

func (*Query) BindParams added in v0.9.0

func (q *Query) BindParams(params Params) *Query

BindParams binds named parameters using Params map. Named parameters are specified using {:name} syntax.

Example:

db.NewQuery("SELECT * FROM users WHERE id = {:id}").
    BindParams(relica.Params{"id": 1}).
    One(&user)

func (*Query) Close added in v0.9.0

func (q *Query) Close() error

Close releases the prepared statement. Safe to call multiple times or on non-prepared queries. Returns nil if query was not prepared or already closed.

func (*Query) Column added in v0.8.0

func (q *Query) Column(slice interface{}) error

Column scans the first column of all rows into a slice. The slice parameter must be a pointer to a slice of the appropriate type.

Example:

var ids []int
err := db.Select("id").From("users").Where("status = ?", "active").Column(&ids)

func (*Query) Execute

func (q *Query) Execute() (sql.Result, error)

Execute runs the query and returns results.

func (*Query) IsPrepared added in v0.9.0

func (q *Query) IsPrepared() bool

IsPrepared returns true if Prepare() was called successfully.

func (*Query) One

func (q *Query) One(dest interface{}) error

One fetches a single row into dest.

func (*Query) Params added in v0.12.0

func (q *Query) Params() []interface{}

Params returns the query parameters.

func (*Query) Prepare added in v0.9.0

func (q *Query) Prepare() *Query

Prepare prepares the query for repeated execution. Call Close() when done to release the prepared statement. The prepared statement bypasses the automatic statement cache, giving you full control over the statement lifecycle.

Example:

q := db.NewQuery("SELECT * FROM users WHERE status = ?").Prepare()
defer q.Close()

for _, status := range statuses {
    var users []User
    q.Bind(status).All(&users)
    // process users...
}

func (*Query) QueryParams deprecated added in v0.9.0

func (q *Query) QueryParams() []interface{}

QueryParams returns the query parameters.

Deprecated: Use Params instead.

func (*Query) Row added in v0.8.0

func (q *Query) Row(dest ...interface{}) error

Row scans a single row into individual variables. Returns sql.ErrNoRows if no rows are found.

Example:

var name string
var age int
err := db.Select("name", "age").From("users").Where("id = ?", 1).Row(&name, &age)

func (*Query) SQL added in v0.9.0

func (q *Query) SQL() string

SQL returns the SQL query string.

type QueryBuilder

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

QueryBuilder constructs type-safe queries.

The query builder provides a fluent interface for building SELECT, INSERT, UPDATE, DELETE, UPSERT, and batch operations. All queries are cached and executed with prepared statements.

Example:

qb := db.Builder()
qb.Select("*").From("users").Where("status = ?", 1).All(&users)

func (*QueryBuilder) BatchInsert

func (qb *QueryBuilder) BatchInsert(table string, columns []string) *BatchInsertQuery

BatchInsert creates a batch INSERT query for multiple rows.

This is 3.3x faster than individual INSERTs for 100 rows. Use Values() or ValuesMap() to add rows.

Example:

db.Builder().BatchInsert("users", []string{"name", "email"}).
    Values("Alice", "alice@example.com").
    Values("Bob", "bob@example.com").
    Execute()

func (*QueryBuilder) BatchInsertStruct added in v0.6.0

func (qb *QueryBuilder) BatchInsertStruct(table string, data interface{}) *Query

BatchInsertStruct builds a batch INSERT query from a slice of structs.

This is a convenience wrapper that converts each struct in the slice to column values. See DB.BatchInsertStruct for full documentation.

Example:

users := []User{
    {Name: "Alice", Email: "alice@example.com"},
    {Name: "Bob", Email: "bob@example.com"},
}
result, err := db.Builder().BatchInsertStruct("users", users).Execute()

func (*QueryBuilder) BatchUpdate

func (qb *QueryBuilder) BatchUpdate(table, keyColumn string) *BatchUpdateQuery

BatchUpdate creates a batch UPDATE query for multiple rows.

This is 2.5x faster than individual UPDATEs for 100 rows. Uses CASE-WHEN logic to update multiple rows with different values.

Example:

db.Builder().BatchUpdate("users", "id").
    Set(1, map[string]interface{}{"status": 2}).
    Set(2, map[string]interface{}{"status": 3}).
    Execute()

func (*QueryBuilder) Delete

func (qb *QueryBuilder) Delete(table string) *DeleteQuery

Delete creates a DELETE query for the specified table.

Use Where() to filter rows to delete.

Example:

db.Builder().Delete("users").
    Where("id = ?", 123).
    Execute()

func (*QueryBuilder) Insert

func (qb *QueryBuilder) Insert(table string, values map[string]interface{}) *Query

Insert builds an INSERT query for a single row.

The values parameter is a map of column names to values. Column order is deterministic (alphabetically sorted) for cache efficiency.

Example:

result, err := db.Builder().Insert("users", map[string]interface{}{
    "name": "Alice",
    "email": "alice@example.com",
    "status": 1,
}).Execute()

func (*QueryBuilder) InsertStruct added in v0.6.0

func (qb *QueryBuilder) InsertStruct(table string, data interface{}) *Query

InsertStruct builds an INSERT query from a struct using db tags.

This is a convenience wrapper around Insert that converts the struct to a map using reflection. See DB.InsertStruct for full documentation.

Example:

user := User{Name: "Alice", Email: "alice@example.com"}
result, err := db.Builder().InsertStruct("users", &user).Execute()

func (*QueryBuilder) Select

func (qb *QueryBuilder) Select(cols ...string) *SelectQuery

Select starts a SELECT query with the specified columns.

If no columns are provided, defaults to "*" (all columns).

Example:

db.Builder().Select("id", "name", "email").From("users").All(&users)

func (*QueryBuilder) Unwrap

func (qb *QueryBuilder) Unwrap() *core.QueryBuilder

Unwrap returns the underlying core.QueryBuilder for advanced use cases.

This method is provided for edge cases where direct access to internal types is needed. Most users should not need this.

func (*QueryBuilder) Update

func (qb *QueryBuilder) Update(table string) *UpdateQuery

Update creates an UPDATE query for the specified table.

Use Set() to specify column values and Where() to filter rows.

Example:

db.Builder().Update("users").
    Set(map[string]interface{}{"status": 2}).
    Where("id = ?", 123).
    Execute()

func (*QueryBuilder) UpdateStruct added in v0.6.0

func (qb *QueryBuilder) UpdateStruct(table string, data interface{}) *UpdateQuery

UpdateStruct builds an UPDATE query from a struct using db tags.

This is a convenience wrapper around Update that converts the struct to a map using reflection. See DB.UpdateStruct for full documentation.

Example:

user := User{Name: "Alice Updated", Status: "active"}
result, err := db.Builder().UpdateStruct("users", &user).
    Where("id = ?", user.ID).
    Execute()

func (*QueryBuilder) Upsert

func (qb *QueryBuilder) Upsert(table string, values map[string]interface{}) *UpsertQuery

Upsert creates an UPSERT query (INSERT with conflict resolution).

Supported strategies:

  • PostgreSQL/SQLite: ON CONFLICT ... DO UPDATE
  • MySQL: ON DUPLICATE KEY UPDATE

Example:

db.Builder().Upsert("users", map[string]interface{}{
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
}).OnConflict("id").DoUpdate("name", "email").Execute()

func (*QueryBuilder) WithContext

func (qb *QueryBuilder) WithContext(ctx context.Context) *QueryBuilder

WithContext sets the context for all queries built by this builder.

The context will be used for all subsequent query operations unless overridden at the query level.

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
qb := db.Builder().WithContext(ctx)
qb.Select("*").From("users").All(&users)

type QueryEvent added in v0.9.0

type QueryEvent = core.QueryEvent

QueryEvent contains information about an executed query. This is passed to QueryHook callbacks for logging, metrics, or tracing.

type QueryHook added in v0.9.0

type QueryHook = core.QueryHook

QueryHook is a callback function invoked after each query execution. Use this for logging, metrics, distributed tracing, or debugging.

type SelectQuery

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

SelectQuery represents a SELECT query being built.

SelectQuery supports a wide range of SQL features including:

  • JOINs (INNER, LEFT, RIGHT, FULL, CROSS)
  • Aggregates (COUNT, SUM, AVG, MIN, MAX)
  • GROUP BY and HAVING
  • ORDER BY, LIMIT, OFFSET
  • Set operations (UNION, INTERSECT, EXCEPT)
  • Common Table Expressions (WITH, WITH RECURSIVE)
  • Subqueries (in FROM, WHERE, SELECT clauses)

Example:

sq := db.Builder().
    Select("u.name", "COUNT(*) as order_count").
    From("users u").
    InnerJoin("orders o", "o.user_id = u.id").
    GroupBy("u.id", "u.name").
    Having("COUNT(*) > ?", 10).
    OrderBy("order_count DESC")
sq.All(&results)

func (*SelectQuery) All

func (sq *SelectQuery) All(dest interface{}) error

All scans all rows into dest slice.

Example:

var users []User
err := db.Builder().Select("*").From("users").All(&users)

func (*SelectQuery) AndSelect added in v0.13.1

func (sq *SelectQuery) AndSelect(cols ...string) *SelectQuery

AndSelect appends additional columns to the SELECT clause. Useful for conditional column building where columns are added based on runtime conditions.

Example:

q := db.Select("id", "name").From("users")
if includeEmail {
    q = q.AndSelect("email")
}
if includePhone {
    q = q.AndSelect("phone")
}

func (*SelectQuery) AndWhere added in v0.8.0

func (sq *SelectQuery) AndWhere(condition interface{}, params ...interface{}) *SelectQuery

AndWhere adds a WHERE condition with AND logic. If no existing WHERE clause exists, behaves like Where().

Example:

db.Builder().Select("*").From("users").
    Where("status = ?", 1).
    AndWhere("age > ?", 18)

func (*SelectQuery) AsExpression

func (sq *SelectQuery) AsExpression() Expression

AsExpression converts a SelectQuery to an Expression for subquery use.

Example:

sub := db.Builder().Select("user_id").From("orders").Where("total > ?", 100)
db.Builder().Select("*").From("users").
    Where(relica.In("id", sub.AsExpression())).All(&users)

func (*SelectQuery) Build

func (sq *SelectQuery) Build() *Query

Build constructs the Query object from SelectQuery.

Example:

q := db.Builder().Select("*").From("users").Where("id = ?", 123).Build()
sql, params := q.SQL(), q.Params()

func (*SelectQuery) Column added in v0.8.0

func (sq *SelectQuery) Column(slice interface{}) error

Column scans the first column of all rows into a slice. The slice parameter must be a pointer to a slice of the appropriate type.

Example:

var ids []int
err := db.Builder().Select("id").From("users").
    Where("status = ?", "active").Column(&ids)

func (*SelectQuery) Count added in v0.11.0

func (sq *SelectQuery) Count() (int64, error)

Count executes a COUNT(*) query and returns the number of matching rows. Any columns specified in Select() are ignored; COUNT(*) is always used.

Example:

count, err := db.Select().From("users").Where(relica.Eq("status", 1)).Count()

func (*SelectQuery) CrossJoin

func (sq *SelectQuery) CrossJoin(table string) *SelectQuery

CrossJoin adds a CROSS JOIN clause (Cartesian product).

Example:

db.Builder().Select("*").
    From("colors").
    CrossJoin("sizes").
    All(&results)

func (*SelectQuery) Distinct added in v0.8.0

func (sq *SelectQuery) Distinct() *SelectQuery

Distinct adds the DISTINCT keyword to the SELECT clause, eliminating duplicate rows.

Example:

db.Builder().Select("category").From("products").Distinct().All(&categories)
// SELECT DISTINCT "category" FROM "products"

func (*SelectQuery) Except

func (sq *SelectQuery) Except(other *SelectQuery) *SelectQuery

Except combines queries using EXCEPT (rows in first but not second).

Database support: PostgreSQL 9.1+, MySQL 8.0.31+, SQLite 3.25+

Example:

q1 := db.Builder().Select("id").From("all_users")
q2 := db.Builder().Select("user_id").From("banned_users")
q1.Except(q2).All(&activeUsers)

func (*SelectQuery) Exists added in v0.11.0

func (sq *SelectQuery) Exists() (bool, error)

Exists executes the query wrapped in SELECT EXISTS(...) and returns true if any rows match.

Example:

exists, err := db.Select().From("users").Where(relica.Eq("email", email)).Exists()

func (*SelectQuery) From

func (sq *SelectQuery) From(table string) *SelectQuery

From specifies the table to select from.

Supports table aliases: From("users u")

Example:

db.Builder().Select("*").From("users").All(&users)

func (*SelectQuery) FromSelect

func (sq *SelectQuery) FromSelect(subquery *SelectQuery, alias string) *SelectQuery

FromSelect specifies a subquery as the FROM source.

The alias parameter is required for the subquery.

Example:

sub := db.Builder().Select("user_id", "COUNT(*) as cnt").
    From("orders").GroupBy("user_id")
db.Builder().Select("*").FromSelect(sub, "order_counts").
    Where("cnt > ?", 10).All(&results)

func (*SelectQuery) FullJoin

func (sq *SelectQuery) FullJoin(table string, on interface{}) *SelectQuery

FullJoin adds a FULL OUTER JOIN clause.

Note: Not supported by MySQL.

Example:

db.Builder().Select("u.name", "o.total").
    From("users u").
    FullJoin("orders o", "o.user_id = u.id").
    All(&results)

func (*SelectQuery) GroupBy

func (sq *SelectQuery) GroupBy(columns ...string) *SelectQuery

GroupBy adds GROUP BY clause.

Multiple columns supported. Multiple GroupBy() calls are additive.

Example:

GroupBy("user_id", "status")

func (*SelectQuery) GroupByExpr added in v0.14.0

func (sq *SelectQuery) GroupByExpr(expr string, args ...interface{}) *SelectQuery

GroupByExpr adds a raw SQL expression to the GROUP BY clause. The expression is used as-is without quoting.

Example:

GroupByExpr("DATE(created_at)")
GroupByExpr("EXTRACT(YEAR FROM order_date)")

func (*SelectQuery) GroupBySub added in v0.14.1

func (sq *SelectQuery) GroupBySub(exp Expression) *SelectQuery

GroupBySub adds a type-safe expression to the GROUP BY clause.

func (*SelectQuery) Having

func (sq *SelectQuery) Having(condition interface{}, args ...interface{}) *SelectQuery

Having adds HAVING clause (WHERE for aggregates).

Accepts string or Expression. Multiple calls are combined with AND.

Example:

Having("COUNT(*) > ?", 100)

func (*SelectQuery) InnerJoin

func (sq *SelectQuery) InnerJoin(table string, on interface{}) *SelectQuery

InnerJoin adds an INNER JOIN clause.

Example:

db.Builder().Select("u.name", "o.total").
    From("users u").
    InnerJoin("orders o", "o.user_id = u.id").
    All(&results)

func (*SelectQuery) Intersect

func (sq *SelectQuery) Intersect(other *SelectQuery) *SelectQuery

Intersect combines queries using INTERSECT (rows in both).

Database support: PostgreSQL 9.1+, MySQL 8.0.31+, SQLite 3.25+

Example:

q1 := db.Builder().Select("id").From("users")
q2 := db.Builder().Select("user_id").From("orders")
q1.Intersect(q2).All(&ids)  // Users who have placed orders

func (*SelectQuery) LeftJoin

func (sq *SelectQuery) LeftJoin(table string, on interface{}) *SelectQuery

LeftJoin adds a LEFT JOIN clause.

Example:

db.Builder().Select("u.name", "o.total").
    From("users u").
    LeftJoin("orders o", "o.user_id = u.id").
    All(&results)

func (*SelectQuery) Limit

func (sq *SelectQuery) Limit(limit int64) *SelectQuery

Limit sets the LIMIT clause.

Example:

Limit(100)  // Return at most 100 rows

func (*SelectQuery) Offset

func (sq *SelectQuery) Offset(offset int64) *SelectQuery

Offset sets the OFFSET clause.

Example:

Offset(200)  // Skip first 200 rows

func (*SelectQuery) One

func (sq *SelectQuery) One(dest interface{}) error

One scans a single row into dest.

Returns sql.ErrNoRows if no row is found.

Example:

var user User
err := db.Builder().Select("*").From("users").
    Where("id = ?", 123).One(&user)

func (*SelectQuery) OrWhere added in v0.8.0

func (sq *SelectQuery) OrWhere(condition interface{}, params ...interface{}) *SelectQuery

OrWhere adds a WHERE condition with OR logic. If no existing WHERE clause exists, behaves like Where().

Example:

db.Builder().Select("*").From("users").
    Where("status = ?", 1).
    OrWhere("role = ?", "admin")

func (*SelectQuery) OrderBy

func (sq *SelectQuery) OrderBy(columns ...string) *SelectQuery

OrderBy adds ORDER BY clause with optional direction (ASC/DESC).

Supports multiple columns. Multiple OrderBy() calls are additive.

Example:

OrderBy("age DESC", "name ASC")

func (*SelectQuery) OrderByExpr added in v0.14.0

func (sq *SelectQuery) OrderByExpr(expr string, args ...interface{}) *SelectQuery

OrderByExpr adds a raw SQL expression to the ORDER BY clause. The expression is used as-is without quoting — useful for CASE WHEN, complex functions, or any expression that shouldn't be treated as a column name.

Example:

OrderByExpr("CASE WHEN status = ? THEN 0 ELSE 1 END", "active")
OrderByExpr("FIELD(id, ?, ?, ?)", 3, 1, 2)

func (*SelectQuery) OrderBySub added in v0.14.1

func (sq *SelectQuery) OrderBySub(exp Expression) *SelectQuery

OrderBySub adds a type-safe expression to the ORDER BY clause. Use with CaseWhen() or any Expression builder instead of raw SQL strings.

Example:

db.Select("id", "title").From("tasks t").
    OrderBySub(relica.CaseWhen().
        When("t.due_date < CURRENT_DATE", 0).
        When("t.due_date IS NULL", 3).
        Else(1)).
    OrderBy("t.due_date ASC").
    All(&rows)

func (*SelectQuery) RightJoin

func (sq *SelectQuery) RightJoin(table string, on interface{}) *SelectQuery

RightJoin adds a RIGHT JOIN clause.

Example:

db.Builder().Select("u.name", "o.total").
    From("users u").
    RightJoin("orders o", "o.user_id = u.id").
    All(&results)

func (*SelectQuery) Row added in v0.8.0

func (sq *SelectQuery) Row(dest ...interface{}) error

Row scans a single row into individual variables. Returns sql.ErrNoRows if no rows are found.

Example:

var name string
var age int
err := db.Builder().Select("name", "age").From("users").
    Where("id = ?", 1).Row(&name, &age)

func (*SelectQuery) SelectExpr

func (sq *SelectQuery) SelectExpr(expr string, args ...interface{}) *SelectQuery

SelectExpr adds a raw SQL expression to the SELECT clause.

Useful for scalar subqueries, window functions, or complex expressions.

Example:

db.Builder().Select("id", "name").
    SelectExpr("(SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id)", "order_count").
    From("users").All(&results)

func (*SelectQuery) SelectSub added in v0.13.0

func (sq *SelectQuery) SelectSub(exp Expression, alias string) *SelectQuery

SelectSub adds a type-safe subquery or computed expression to the SELECT clause with a quoted alias. The expression is built with the correct dialect so identifiers are properly quoted. Prefer SelectSub over SelectExpr when you have an Expression (e.g. a correlated subquery).

Example:

sub := db.Builder().Select("COUNT(*)").From("orders").Where(relica.EqCol("orders.user_id", "users.id"))
db.Builder().Select("id", "name").SelectSub(sub.AsExpression(), "order_count").From("users").All(&results)

Generates (PostgreSQL):

SELECT "id", "name", (SELECT COUNT(*) FROM "orders" WHERE "orders"."user_id" = "users"."id") AS "order_count" FROM "users"

func (*SelectQuery) ToSQL added in v0.11.0

func (sq *SelectQuery) ToSQL() (string, []interface{})

ToSQL returns the SQL string and parameters without executing the query. This is useful for debugging, logging, or passing the query to another layer.

Example:

sql, params := db.Select().From("users").Where(relica.Eq("id", 1)).ToSQL()

func (*SelectQuery) Union

func (sq *SelectQuery) Union(other *SelectQuery) *SelectQuery

Union combines this query with another using UNION (removes duplicates).

Example:

q1 := db.Builder().Select("name").From("users")
q2 := db.Builder().Select("name").From("archived_users")
q1.Union(q2).All(&names)

func (*SelectQuery) UnionAll

func (sq *SelectQuery) UnionAll(other *SelectQuery) *SelectQuery

UnionAll combines this query with another using UNION ALL (keeps duplicates).

Example:

q1 := db.Builder().Select("id").From("orders_2023")
q2 := db.Builder().Select("id").From("orders_2024")
q1.UnionAll(q2).All(&orderIDs)

func (*SelectQuery) Unwrap

func (sq *SelectQuery) Unwrap() *core.SelectQuery

Unwrap returns the underlying core.SelectQuery for advanced use cases.

This method is provided for edge cases where direct access to internal types is needed. Most users should not need this.

func (*SelectQuery) Where

func (sq *SelectQuery) Where(condition interface{}, params ...interface{}) *SelectQuery

Where adds a WHERE condition.

Accepts either a string with placeholders or an Expression. Multiple Where() calls are combined with AND.

String example:

Where("status = ? AND age > ?", 1, 18)

Expression example:

Where(relica.And(
    relica.Eq("status", 1),
    relica.GreaterThan("age", 18),
))

func (*SelectQuery) With

func (sq *SelectQuery) With(name string, query *SelectQuery) *SelectQuery

With adds a Common Table Expression (CTE).

Example:

cte := db.Builder().Select("user_id", "SUM(total) as total").
    From("orders").GroupBy("user_id")
db.Builder().Select("*").With("order_totals", cte).
    From("order_totals").Where("total > ?", 1000).All(&users)

func (*SelectQuery) WithContext

func (sq *SelectQuery) WithContext(ctx context.Context) *SelectQuery

WithContext sets the context for this SELECT query.

This overrides any context set on the QueryBuilder.

Example:

sq.WithContext(ctx).All(&users)

func (*SelectQuery) WithRecursive

func (sq *SelectQuery) WithRecursive(name string, query *SelectQuery) *SelectQuery

WithRecursive adds a recursive Common Table Expression.

The query MUST use UNION or UNION ALL. Database support: PostgreSQL (all), MySQL 8.0+, SQLite 3.25+

Example:

anchor := db.Builder().Select("id", "name", "manager_id", "1 as level").
    From("employees").Where("manager_id IS NULL")
recursive := db.Builder().Select("e.id", "e.name", "e.manager_id", "h.level + 1").
    From("employees e").InnerJoin("hierarchy h", "e.manager_id = h.id")
cte := anchor.UnionAll(recursive)
db.Builder().Select("*").WithRecursive("hierarchy", cte).
    From("hierarchy").OrderBy("level", "name").All(&employees)

type SlogAdapter

type SlogAdapter = logger.SlogAdapter

SlogAdapter wraps log/slog.Logger to implement the Logger interface.

func NewSlogAdapter

func NewSlogAdapter(l *slog.Logger) *SlogAdapter

NewSlogAdapter creates a new logger adapter wrapping an slog.Logger.

type Tx

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

Tx represents a database transaction.

Transactions provide ACID guarantees and support all standard isolation levels. All queries executed through a transaction's builder automatically participate in that transaction.

Example:

tx, err := db.Begin(ctx)
if err != nil {
    return err
}
defer tx.Rollback() // Safe to call even after Commit

_, err = tx.Insert("users", data).Execute()
if err != nil {
    return err
}

return tx.Commit()

func (*Tx) BatchInsert added in v0.12.0

func (t *Tx) BatchInsert(table string, columns []string) *BatchInsertQuery

BatchInsert creates a new batch INSERT query within the transaction.

This is a convenience method equivalent to tx.Builder().BatchInsert(table, columns).

Example:

result, err := tx.BatchInsert("users", []string{"name", "email"}).
    Values("Alice", "alice@example.com").
    Values("Bob", "bob@example.com").
    Execute()

func (*Tx) BatchInsertStruct added in v0.6.0

func (t *Tx) BatchInsertStruct(table string, data interface{}) *Query

BatchInsertStruct creates a batch INSERT query from structs within the transaction.

This is a convenience method equivalent to tx.Builder().BatchInsertStruct(table, data). See DB.BatchInsertStruct for full documentation.

Example:

users := []User{
    {Name: "Alice", Email: "alice@example.com"},
    {Name: "Bob", Email: "bob@example.com"},
}
_, err := tx.BatchInsertStruct("users", users).Execute()
if err != nil {
    tx.Rollback()
    return err
}

func (*Tx) BatchUpdate added in v0.12.0

func (t *Tx) BatchUpdate(table, keyColumn string) *BatchUpdateQuery

BatchUpdate creates a new batch UPDATE query within the transaction.

This is a convenience method equivalent to tx.Builder().BatchUpdate(table, keyColumn).

Example:

result, err := tx.BatchUpdate("users", "id").
    Set(1, map[string]interface{}{"name": "Alice"}).
    Set(2, map[string]interface{}{"name": "Bob"}).
    Execute()

func (*Tx) Builder

func (t *Tx) Builder() *QueryBuilder

Builder returns the query builder for this transaction.

All queries built using this builder will execute within the transaction. The builder automatically inherits the transaction's context.

Example:

tx.Builder().Insert("users", data).Execute()

func (*Tx) Commit

func (t *Tx) Commit() error

Commit commits the transaction.

After calling Commit, the transaction cannot be used for further queries.

Example:

if err := tx.Commit(); err != nil {
    return err
}

func (*Tx) Delete

func (t *Tx) Delete(table string) *DeleteQuery

Delete creates a new DELETE query within the transaction.

This is a convenience method equivalent to tx.Builder().Delete(table).

Example:

_, err := tx.Delete("users").Where("id = ?", 123).Execute()
if err != nil {
    tx.Rollback()
    return err
}

func (*Tx) Insert

func (t *Tx) Insert(table string, data map[string]interface{}) *Query

Insert creates a new INSERT query within the transaction.

This is a convenience method equivalent to tx.Builder().Insert(table, data).

Example:

_, err := tx.Insert("users", map[string]interface{}{
    "name":  "Alice",
    "email": "alice@example.com",
}).Execute()
if err != nil {
    tx.Rollback()
    return err
}

func (*Tx) InsertStruct added in v0.6.0

func (t *Tx) InsertStruct(table string, data interface{}) *Query

InsertStruct creates an INSERT query from a struct within the transaction.

This is a convenience method equivalent to tx.Builder().InsertStruct(table, data). See DB.InsertStruct for full documentation.

Example:

user := User{Name: "Alice", Email: "alice@example.com"}
_, err := tx.InsertStruct("users", &user).Execute()
if err != nil {
    tx.Rollback()
    return err
}

func (*Tx) Model added in v0.6.0

func (t *Tx) Model(model interface{}) *ModelQuery

Model creates a ModelQuery within transaction context.

All operations performed through this ModelQuery will execute within the transaction.

Example:

tx, _ := db.Begin(ctx)
defer tx.Rollback()

user := User{Name: "Alice"}
err := tx.Model(&user).Insert()
if err != nil {
    return err
}

return tx.Commit()

func (*Tx) NewQuery added in v0.12.0

func (t *Tx) NewQuery(query string) *Query

NewQuery creates a raw SQL query within the transaction.

This is a convenience method equivalent to tx.Builder()-based raw query execution.

Example:

var count int
err := tx.NewQuery("SELECT COUNT(*) FROM users").Row(&count)

func (*Tx) Rollback

func (t *Tx) Rollback() error

Rollback rolls back the transaction.

After calling Rollback, the transaction cannot be used for further queries. It's safe to call Rollback even after Commit (it will be a no-op).

Example:

defer tx.Rollback() // Safe even after Commit

func (*Tx) Select

func (t *Tx) Select(cols ...string) *SelectQuery

Select creates a new SELECT query within the transaction.

This is a convenience method equivalent to tx.Builder().Select(cols...).

Example:

var users []User
err := tx.Select("*").From("users").Where("id = ?", 123).All(&users)
if err != nil {
    tx.Rollback()
    return err
}

// For advanced features, use Builder()
err := tx.Builder().
    With("stats", statsQuery).
    Select("*").
    From("stats").
    All(&results)

func (*Tx) Unwrap

func (t *Tx) Unwrap() *core.Tx

Unwrap returns the underlying core.Tx for advanced use cases.

This method is provided for edge cases where direct access to internal types is needed. Most users should not need this.

func (*Tx) Update

func (t *Tx) Update(table string) *UpdateQuery

Update creates a new UPDATE query within the transaction.

This is a convenience method equivalent to tx.Builder().Update(table).

Example:

_, err := tx.Update("users").
    Set(map[string]interface{}{"status": "active"}).
    Where("id = ?", 123).
    Execute()
if err != nil {
    tx.Rollback()
    return err
}

func (*Tx) UpdateStruct added in v0.6.0

func (t *Tx) UpdateStruct(table string, data interface{}) *UpdateQuery

UpdateStruct creates an UPDATE query from a struct within the transaction.

This is a convenience method equivalent to tx.Builder().UpdateStruct(table, data). See DB.UpdateStruct for full documentation.

Example:

user := User{Name: "Alice Updated", Status: "active"}
_, err := tx.UpdateStruct("users", &user).
    Where("id = ?", user.ID).
    Execute()
if err != nil {
    tx.Rollback()
    return err
}

func (*Tx) Upsert added in v0.12.0

func (t *Tx) Upsert(table string, values map[string]interface{}) *UpsertQuery

Upsert creates a new UPSERT query within the transaction.

This is a convenience method equivalent to tx.Builder().Upsert(table, values).

Example:

result, err := tx.Upsert("users", map[string]interface{}{
    "email": "alice@example.com",
    "name":  "Alice",
}).OnConflict("email").DoUpdate("name").Execute()

type TxOptions

type TxOptions = core.TxOptions

TxOptions represents transaction options including isolation level.

Example:

opts := &relica.TxOptions{
    Isolation: sql.LevelSerializable,
    ReadOnly:  true,
}
tx, err := db.BeginTx(ctx, opts)

type UpdateQuery

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

UpdateQuery represents an UPDATE query being built.

func (*UpdateQuery) AndWhere added in v0.8.0

func (uq *UpdateQuery) AndWhere(condition interface{}, params ...interface{}) *UpdateQuery

AndWhere adds a WHERE condition with AND logic. If no existing WHERE clause exists, behaves like Where().

Example:

db.Builder().Update("users").
    Set(map[string]interface{}{"status": 2}).
    Where("id > ?", 100).
    AndWhere("active = ?", true)

func (*UpdateQuery) Build

func (uq *UpdateQuery) Build() *Query

Build constructs the Query object.

func (*UpdateQuery) Execute

func (uq *UpdateQuery) Execute() (sql.Result, error)

Execute executes the UPDATE query.

func (*UpdateQuery) OrWhere added in v0.8.0

func (uq *UpdateQuery) OrWhere(condition interface{}, params ...interface{}) *UpdateQuery

OrWhere adds a WHERE condition with OR logic. If no existing WHERE clause exists, behaves like Where().

Example:

db.Builder().Update("users").
    Set(map[string]interface{}{"status": 0}).
    Where("banned = ?", true).
    OrWhere("deleted = ?", true)

func (*UpdateQuery) Set

func (uq *UpdateQuery) Set(values map[string]interface{}) *UpdateQuery

Set specifies the columns and values to update.

Example:

Update("users").Set(map[string]interface{}{"status": 2})

func (*UpdateQuery) ToSQL added in v0.11.0

func (uq *UpdateQuery) ToSQL() (string, []interface{})

ToSQL returns the SQL string and parameters without executing the query. This is useful for debugging, logging, or passing the query to another layer.

Example:

sql, params := db.Update("users").Set(map[string]interface{}{"status": 1}).Where(relica.Eq("id", 1)).ToSQL()

func (*UpdateQuery) Where

func (uq *UpdateQuery) Where(condition interface{}, params ...interface{}) *UpdateQuery

Where adds a WHERE condition to the UPDATE query.

Example:

Update("users").Set(...).Where("id = ?", 123)

func (*UpdateQuery) WithContext

func (uq *UpdateQuery) WithContext(ctx context.Context) *UpdateQuery

WithContext sets the context for this UPDATE query.

type UpsertQuery

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

UpsertQuery represents an UPSERT query being built.

func (*UpsertQuery) Build

func (uq *UpsertQuery) Build() *Query

Build constructs the Query object.

func (*UpsertQuery) DoNothing

func (uq *UpsertQuery) DoNothing() *UpsertQuery

DoNothing ignores conflicts (no update).

Example:

Upsert(...).OnConflict("id").DoNothing()

func (*UpsertQuery) DoUpdate

func (uq *UpsertQuery) DoUpdate(columns ...string) *UpsertQuery

DoUpdate specifies which columns to update on conflict.

Example:

Upsert(...).OnConflict("id").DoUpdate("name", "email")

func (*UpsertQuery) Execute

func (uq *UpsertQuery) Execute() (sql.Result, error)

Execute executes the UPSERT query.

func (*UpsertQuery) OnConflict

func (uq *UpsertQuery) OnConflict(columns ...string) *UpsertQuery

OnConflict specifies the columns that determine a conflict.

Example:

Upsert(...).OnConflict("id", "email")

func (*UpsertQuery) ToSQL added in v0.12.0

func (uq *UpsertQuery) ToSQL() (string, []interface{})

ToSQL returns the SQL string and parameters without executing the query. This is useful for debugging, logging, or passing the query to another layer.

Example:

sql, params := db.Upsert("users", map[string]interface{}{"id": 1, "name": "Alice"}).
    OnConflict("id").DoUpdate("name").ToSQL()

func (*UpsertQuery) WithContext

func (uq *UpsertQuery) WithContext(ctx context.Context) *UpsertQuery

WithContext sets the context for this UPSERT query.

Directories

Path Synopsis
examples module
internal
analyzer
Package analyzer provides database query plan analysis using EXPLAIN functionality.
Package analyzer provides database query plan analysis using EXPLAIN functionality.
cache
Package cache provides caching utilities for database prepared statements.
Package cache provides caching utilities for database prepared statements.
core
Package core provides the core database functionality including connection management, query building, statement caching, and result scanning for Relica.
Package core provides the core database functionality including connection management, query building, statement caching, and result scanning for Relica.
dialects
Package dialects provides database-specific SQL dialect implementations for PostgreSQL, MySQL, and SQLite, handling identifier quoting, placeholders, and UPSERT operations.
Package dialects provides database-specific SQL dialect implementations for PostgreSQL, MySQL, and SQLite, handling identifier quoting, placeholders, and UPSERT operations.
logger
Package logger provides logging abstractions for Relica.
Package logger provides logging abstractions for Relica.
optimizer
Package optimizer provides query optimization analysis and suggestions.
Package optimizer provides query optimization analysis and suggestions.
security
Package security provides SQL injection prevention, parameter validation, and query security features for Relica.
Package security provides SQL injection prevention, parameter validation, and query security features for Relica.
util
Package util provides utility functions for context handling, string sanitization, and reflection helpers used throughout the Relica library.
Package util provides utility functions for context handling, string sanitization, and reflection helpers used throughout the Relica library.

Jump to

Keyboard shortcuts

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