relica

package module
v0.9.1 Latest Latest
Warning

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

Go to latest
Published: Dec 22, 2025 License: MIT Imports: 8 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 - 650+ tests, 86%+ 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() 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 = ?", val) Simple parameterized queries
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()

    // For advanced queries (CTEs, UNION, etc.), use Builder()
    err = db.Builder().
        With("stats", statsQuery).
        Select("*").
        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.Builder().Select().From("users").
    Where(relica.HashExp{"status": 1}).
    All(&users)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

String-based WHERE still works:

// Old style (still supported)
db.Builder().Select().From("users").
    Where("status = ? AND age > ?", 1, 18).
    All(&users)

// Can mix both styles
db.Builder().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: db:"pk" tag → "ID" field → "Id" field

type Product struct {
    ID    int    `db:"pk"`    // Explicit PK marking
    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
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.Builder().
    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.Builder().
    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.Builder().InnerJoin(table, on)  // INNER JOIN
db.Builder().LeftJoin(table, on)   // LEFT OUTER JOIN
db.Builder().RightJoin(table, on)  // RIGHT OUTER JOIN
db.Builder().FullJoin(table, on)   // FULL OUTER JOIN (PostgreSQL, SQLite)
db.Builder().CrossJoin(table)      // CROSS JOIN (no ON condition)

// JOIN with Expression API
db.Builder().
    Select().
    From("messages m").
    InnerJoin("users u", relica.And(
        relica.Raw("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 JOIN Guide for comprehensive 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.Builder().
    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.Builder().
    Select().
    From("users").
    OrderBy("age DESC").
    Limit(pageSize).
    Offset(pageNumber * pageSize).
    All(&users)

// Table column references
db.Builder().
    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.Builder().
    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.Builder().
    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.Builder().
    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 Aggregates Guide for comprehensive examples and patterns.

Advanced SQL Features

Relica adds powerful SQL features for complex queries.

Subqueries

IN/EXISTS Subqueries:

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

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

FROM Subqueries:

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

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

See Subquery Guide for complete examples and performance tips.

Set Operations

UNION/UNION ALL:

// Combine active and archived users (UNION removes duplicates)
active := db.Builder().Select("name").From("users").Where("status = ?", 1)
archived := db.Builder().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.Builder().Select("id").From("users")
orderUsers := db.Builder().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.Builder().
    Select("user_id", "SUM(total) as total").
    From("orders").
    GroupBy("user_id")

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

Recursive CTEs (organizational hierarchies, trees):

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

// Recursive: children
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")

// Build hierarchy
db.Builder().
    WithRecursive("hierarchy", anchor.UnionAll(recursive)).
    Select("*").
    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.Builder().
    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.Builder().
    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.Builder().Insert("users", userData).Execute()
if err != nil {
    return err
}

_, err = tx.Builder().
    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.Builder().
    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.Builder().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.Builder().
    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.Builder().
    WithContext(ctx).
    Select().
    From("users").
    All(&users)

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

// Transaction context auto-propagates
tx, _ := db.BeginTx(ctx, nil)
tx.Builder().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 cache statistics
stats := db.stmtCache.Stats()
fmt.Printf("Cache hit rate: %.2f%%\n", stats.HitRate*100)
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.Builder().
    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:

import "github.com/coregx/relica/internal/security"

// Create validator
validator := security.NewValidator()

// Enable validation on DB connection
db, err := relica.Open("postgres", dsn,
    relica.WithValidator(validator),
)

// All ExecContext and QueryContext calls are now validated
_, err = db.ExecContext(ctx, "SELECT * FROM users WHERE id = ?", userID)
// Malicious queries blocked: stacked queries, UNION attacks, comment injection, etc.

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:

// Create logger
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))

// Create auditor with desired level
auditor := security.NewAuditor(logger, security.AuditReads)

// Enable auditing
db, err := relica.Open("postgres", dsn,
    relica.WithAuditLog(auditor),
)

// Add context metadata for forensics
ctx := security.WithUser(ctx, "john.doe@example.com")
ctx = security.WithClientIP(ctx, "192.168.1.100")
ctx = security.WithRequestID(ctx, "req-12345")

// All operations are logged with metadata
_, err = db.ExecContext(ctx, "UPDATE users SET status = ? WHERE id = ?", 2, 123)

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: 93.3%
  • Dependencies: 0 (production), 2 (tests only)
  • 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 COREGX Team

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.Builder().Select("*").From("users").All(&users)

Features

CRUD Operations:

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

// INSERT
db.Builder().Insert("users", map[string]interface{}{
    "name": "Alice",
    "email": "alice@example.com",
}).Execute()

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

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

Index

Constants

This section is empty.

Variables

View Source
var And = core.And

And combines expressions with AND.

View Source
var Between = core.Between

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

View Source
var Case = core.Case

Case creates a simple CASE expression.

View Source
var CaseWhen = core.CaseWhen

CaseWhen creates a searched CASE expression (without column).

View Source
var Coalesce = core.Coalesce

Coalesce creates a COALESCE expression.

View Source
var Concat = core.Concat

Concat creates a string concatenation expression.

View Source
var DetectOperation = core.DetectOperation

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

View Source
var Eq = core.Eq

Eq creates an equality expression (column = value).

View Source
var Exists = core.Exists

Exists creates an EXISTS subquery expression.

View Source
var GreaterOrEqual = core.GreaterOrEqual

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

View Source
var GreaterThan = core.GreaterThan

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

View Source
var Greatest = core.Greatest

Greatest creates a GREATEST expression.

View Source
var In = core.In

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

View Source
var Least = core.Least

Least creates a LEAST expression.

View Source
var LessOrEqual = core.LessOrEqual

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

View Source
var LessThan = core.LessThan

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

View Source
var Like = core.Like

Like creates a LIKE expression with automatic escaping.

View Source
var NewExp = core.NewExp

NewExp creates a new raw SQL expression.

View Source
var NewSlogAdapter = logger.NewSlogAdapter

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

View Source
var Not = core.Not

Not negates an expression.

View Source
var NotBetween = core.NotBetween

NotBetween creates a NOT BETWEEN expression.

View Source
var NotEq = core.NotEq

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

View Source
var NotExists = core.NotExists

NotExists creates a NOT EXISTS subquery expression.

View Source
var NotIn = core.NotIn

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

View Source
var NotLike = core.NotLike

NotLike creates a NOT LIKE expression.

View Source
var NullIf = core.NullIf

NullIf creates a NULLIF expression.

View Source
var Or = core.Or

Or combines expressions with OR.

View Source
var OrLike = core.OrLike

OrLike creates a LIKE expression combined with OR.

View Source
var OrNotLike = core.OrNotLike

OrNotLike creates a NOT LIKE expression combined with OR.

View Source
var WithConnMaxIdleTime = core.WithConnMaxIdleTime

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))
View Source
var WithConnMaxLifetime = core.WithConnMaxLifetime

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))
View Source
var WithHealthCheck = core.WithHealthCheck

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))
View Source
var WithLogger = core.WithLogger

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)))
View Source
var WithMaxIdleConns = core.WithMaxIdleConns

WithMaxIdleConns sets the maximum number of idle connections.

View Source
var WithMaxOpenConns = core.WithMaxOpenConns

WithMaxOpenConns sets the maximum number of open connections.

View Source
var WithQueryHook = core.WithQueryHook

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)
    }))
View Source
var WithSensitiveFields = core.WithSensitiveFields

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"}))
View Source
var WithStmtCacheCapacity = core.WithStmtCacheCapacity

WithStmtCacheCapacity sets the prepared statement cache capacity.

Functions

This section is empty.

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) 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) 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.

type CoalesceExp added in v0.9.0

type CoalesceExp = core.CoalesceExp

CoalesceExp represents a SQL COALESCE expression.

type ConcatExp added in v0.9.0

type ConcatExp = core.ConcatExp

ConcatExp represents a SQL 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

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

NewDB creates a database connection (deprecated: use Open).

This function exists for backward compatibility. New code should use Open.

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) 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) 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.Builder().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() string

GenerateParamName generates a unique parameter placeholder name.

This is useful when building dynamic SQL queries.

Example:

ph := db.GenerateParamName()
// Returns: p1, p2, p3, etc.

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) 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) 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)

type GreatestLeastExp added in v0.9.0

type GreatestLeastExp = core.GreatestLeastExp

GreatestLeastExp represents a SQL GREATEST or 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)

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=?

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.

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))

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) 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 added in v0.9.0

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

QueryParams returns the query parameters.

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) 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) 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(v bool) *SelectQuery

Distinct sets whether to select distinct rows. When enabled, adds DISTINCT keyword to the SELECT clause to eliminate duplicate rows. Multiple calls to Distinct() override previous settings.

Example:

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

db.Builder().Select("*").From("users").Distinct(false).All(&users)
// SELECT * FROM "users"

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) 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) 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) 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) 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.

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.Builder().Insert("users", data).Execute()
if err != nil {
    return err
}

return tx.Commit()

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) 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) 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
}

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) 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) WithContext

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

WithContext sets the context for this UPSERT query.

Directories

Path Synopsis
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