Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:58:35 +08:00
commit 322d28c7eb
14 changed files with 7534 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "cool-mysql",
"description": "Comprehensive guide for cool-mysql Go library - MySQL helper with dual connection pools, named parameters, template syntax, caching, and advanced query patterns",
"version": "0.0.0-2025.11.28",
"author": {
"name": "Stirling Marketing Group",
"email": "zhongweili@tubi.tv"
},
"skills": [
"./"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# cool-mysql
Comprehensive guide for cool-mysql Go library - MySQL helper with dual connection pools, named parameters, template syntax, caching, and advanced query patterns

518
SKILL.md Normal file
View File

@@ -0,0 +1,518 @@
---
name: cool-mysql
description: Use this skill when working with the cool-mysql library for Go. This skill provides comprehensive guidance on using cool-mysql's MySQL helper functions, including dual connection pools, named parameters, template syntax, caching strategies, and advanced query patterns. Apply when writing database code, optimizing queries, setting up caching, or migrating from database/sql.
---
# cool-mysql MySQL Helper Library
## Overview
`cool-mysql` is a MySQL helper library for Go that wraps `database/sql` with MySQL-specific conveniences while keeping the underlying interfaces intact. The library reduces boilerplate code for common database operations while providing advanced features like caching, automatic retries, and dual read/write connection pools.
**Core Philosophy:**
- Keep `database/sql` interfaces intact
- Provide conveniences without hiding MySQL behavior
- Focus on productivity without sacrificing control
- Type-safe operations with flexible result mapping
## When to Use This Skill
Use this skill when:
- Writing MySQL database operations in Go
- Setting up database connections with read/write separation
- Implementing caching strategies for queries
- Working with struct mappings and MySQL columns
- Migrating from `database/sql` to `cool-mysql`
- Optimizing query performance
- Handling transactions with proper context management
- Debugging query issues or understanding error handling
- Implementing CRUD operations, upserts, or batch inserts
## Core Concepts
### 1. Dual Connection Pools
`cool-mysql` maintains separate connection pools for reads and writes to optimize for read-heavy workloads.
**Default Behavior:**
- `Select()`, `SelectJSON()`, `Count()`, `Exists()` → Read pool
- `Insert()`, `Upsert()`, `Exec()` → Write pool
- `SelectWrites()`, `ExistsWrites()` → Write pool (for read-after-write consistency)
**When to use SelectWrites():**
Use immediately after writing data when you need consistency:
```go
db.Insert("users", user)
// Need immediate consistency - use write pool
db.SelectWrites(&user, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id", 0, user.ID)
```
### 2. Named Parameters
cool-mysql uses `@@paramName` syntax instead of positional `?` placeholders.
**Key Points:**
- Parameters are case-insensitive when merged
- Structs can be used directly as parameters (field names → parameter names)
- Use `mysql.Params{"key": value}` for explicit parameters
- Use `mysql.Raw()` to inject literal SQL (not escaped)
**Example:**
```go
// Named parameters
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `age` > @@minAge AND `status` = @@status", 0,
mysql.Params{"minAge": 18, "status": "active"})
// Struct as parameters
user := User{ID: 1, Name: "Alice"}
db.Exec("UPDATE `users` SET `name` = @@Name WHERE `id` = @@ID", user)
// Raw SQL injection
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE @@condition", 0,
mysql.Raw("created_at > NOW() - INTERVAL 1 DAY"))
```
### 3. Template Syntax
cool-mysql supports Go template syntax for conditional query logic.
**Important Distinctions:**
- Template variables use **field names** (`.Name`), not column names from tags
- Template processing happens **before** parameter interpolation
- Access parameters directly as fields: `.ParamName`
**CRITICAL: Marshaling Template Values**
When injecting VALUES (not identifiers) via templates, you MUST use the `marshal` pipe:
```go
// ✅ CORRECT - Use @@param for values (automatically marshaled)
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE {{ if .MinAge }}`age` > @@minAge{{ end }}"
// ✅ CORRECT - Use | marshal when injecting value directly in template
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `name` = {{ .Name | marshal }}"
// ❌ WRONG - Direct injection without marshal causes syntax errors
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `name` = {{ .Name }}" // BROKEN!
// ✅ CORRECT - Identifiers (column names) validated, then injected
if !allowedColumns[sortBy] { return errors.New("invalid column") }
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` ORDER BY {{ .SortBy }}" // OK - validated identifier
```
**Best Practice:** Use `@@param` syntax for values. Only use template injection with `| marshal` when you need conditional value logic.
**Example:**
```go
db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE 1=1"+
" {{ if .MinAge }}AND `age` > @@minAge{{ end }}"+
" {{ if .Status }}AND `status` = @@status{{ end }}",
0,
mysql.Params{"minAge": 18, "status": "active"})
```
### 4. Caching
cool-mysql provides pluggable caching with support for Redis, Memcached, or in-memory storage.
**Cache TTL:**
- `0` = No caching (always query database)
- `> 0` = Cache for specified duration (e.g., `5*time.Minute`)
**Cache Setup:**
```go
// Redis (with distributed locking)
db.EnableRedis(redisClient)
// Memcached
db.EnableMemcache(memcacheClient)
// In-memory (weak pointers, GC-managed)
db.UseCache(mysql.NewWeakCache())
// Layered caching (fast local + shared distributed)
db.UseCache(mysql.NewMultiCache(
mysql.NewWeakCache(), // L1: Fast local cache
mysql.NewRedisCache(redis), // L2: Shared distributed cache
))
```
**Only SELECT operations are cached** - writes always hit the database.
### 5. Struct Tag Mapping
Control column mapping and behavior with `mysql` struct tags.
**Tag Options:**
- `mysql:"column_name"` - Map to database column
- `mysql:"column_name,defaultzero"` - Write `DEFAULT(column_name)` for zero values
- `mysql:"column_name,omitempty"` - Same as `defaultzero`
- `mysql:"column_name,insertDefault"` - Same as `defaultzero`
- `mysql:"-"` - Completely ignore this field
- `mysql:"column0x2cname"` - Hex encoding for special characters (becomes `column,name`)
**Example:**
```go
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Email string `mysql:"email"`
CreatedAt time.Time `mysql:"created_at,defaultzero"` // Use DB default on zero value
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
Password string `mysql:"-"` // Never include in queries
}
```
## Quick Start Guide
### Creating a Database Connection
**From connection parameters:**
```go
db, err := mysql.New(
wUser, wPass, wSchema, wHost, wPort, // Write connection
rUser, rPass, rSchema, rHost, rPort, // Read connection
collation, // e.g., "utf8mb4_unicode_ci"
timeZone, // e.g., "America/New_York"
)
```
**From DSN strings:**
```go
db, err := mysql.NewFromDSN(writesDSN, readsDSN)
```
**From existing connections:**
```go
db, err := mysql.NewFromConn(writesConn, readsConn)
```
### Basic Query Patterns
**Select into struct slice:**
```go
var users []User
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `age` > @@minAge", 0, 18)
```
**Select single value:**
```go
var name string
err := db.Select(&name, "SELECT `name` FROM `users` WHERE `id` = @@id", 0, 1)
// Returns sql.ErrNoRows if not found
```
**Count records:**
```go
count, err := db.Count("SELECT COUNT(*) FROM `users` WHERE `active` = @@active", 0, 1)
```
**Check existence:**
```go
exists, err := db.Exists("SELECT 1 FROM `users` WHERE `email` = @@email", 0, "user@example.com")
```
**Insert data:**
```go
// Single insert
user := User{Name: "Alice", Email: "alice@example.com"}
err := db.Insert("users", user)
// Batch insert (automatically chunked)
users := []User{{Name: "Bob"}, {Name: "Charlie"}}
err := db.Insert("users", users)
```
**Upsert (INSERT ... ON DUPLICATE KEY UPDATE):**
```go
err := db.Upsert(
"users", // table
[]string{"email"}, // unique columns
[]string{"name", "updated_at"}, // columns to update on conflict
"", // optional WHERE clause
user, // data
)
```
**Execute query:**
```go
err := db.Exec("UPDATE `users` SET `active` = 1 WHERE `id` = @@id", 1)
```
## Migration Guide from database/sql
### Key Differences
| database/sql | cool-mysql | Notes |
|--------------|------------|-------|
| `?` placeholders | `@@paramName` | Named parameters are case-insensitive |
| `db.Query()` + `rows.Scan()` | `db.Select(&result, query, cacheTTL, params)` | Automatic scanning into structs |
| Manual connection pools | Dual pools (read/write) | Automatic routing based on operation |
| No caching | Built-in caching | Pass TTL as second parameter |
| `sql.ErrNoRows` always | `sql.ErrNoRows` for single values only | Slices return empty, not error |
| Manual chunking | Automatic chunking | Insert operations respect `max_allowed_packet` |
| No retry logic | Automatic retries | Handles deadlocks, timeouts, connection losses |
### Migration Pattern
**Before (database/sql):**
```go
rows, err := db.Query("SELECT `id`, `name`, `email` FROM `users` WHERE `age` > ?", 18)
if err != nil {
return err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return err
}
users = append(users, u)
}
return rows.Err()
```
**After (cool-mysql):**
```go
var users []User
return db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `age` > @@minAge", 0, 18)
```
## Best Practices
### Parameter Handling
**DO:**
- Use `@@paramName` syntax consistently
- Use `mysql.Params{}` for clarity
- Use structs as parameters when appropriate
- Use `mysql.Raw()` for literal SQL that shouldn't be escaped
**DON'T:**
- Mix `?` and `@@` syntax (use `@@` exclusively)
- Assume parameters are case-sensitive (they're normalized)
- Inject user input with `mysql.Raw()` (SQL injection risk)
### Template Usage
**DO:**
- Use templates for conditional query logic
- Use `@@param` for values (preferred - automatically marshaled)
- Use `{{.Field | marshal}}` when injecting values directly in templates
- Validate/whitelist identifiers (column names) before template injection
- Reference parameters by field name: `.ParamName`
- Add custom template functions with `db.AddTemplateFuncs()`
**DON'T:**
- Inject values without marshal: `{{.Name}}` causes syntax errors
- Use column names in templates (use field names)
- Forget that templates process before parameter interpolation
- Use templates when named parameters suffice
- Inject user-controlled identifiers without validation
### Caching Strategy
**DO:**
- Use `0` TTL for frequently-changing data
- Use longer TTLs (5-60 minutes) for stable reference data
- Use `SelectWrites()` immediately after writes for consistency
- Consider `MultiCache` for high-traffic applications
- Enable Redis distributed locking to prevent cache stampedes
**DON'T:**
- Cache writes (they're automatically skipped)
- Use same TTL for all queries (tune based on data volatility)
- Forget that cache keys include query + parameters
### Struct Tags
**DO:**
- Use `defaultzero` for timestamp columns with DB defaults
- Use `mysql:"-"` to exclude sensitive fields
- Use hex encoding for column names with special characters
- Implement `Zeroer` interface for custom zero-value detection
**DON'T:**
- Forget that tag column names override field names
- Mix `json` tags with `mysql` tags without testing
### Error Handling
**DO:**
- Check for `sql.ErrNoRows` when selecting single values
- Rely on automatic retries for transient errors (deadlocks, timeouts)
- Use `ExecResult()` when you need `LastInsertId()` or `RowsAffected()`
**DON'T:**
- Expect `sql.ErrNoRows` when selecting into slices (returns empty slice)
- Implement manual retry logic (already built-in)
### Performance Optimization
**DO:**
- Use channels for memory-efficient streaming of large datasets
- Use `SelectWrites()` sparingly (only when consistency required)
- Enable caching for expensive or frequent queries
- Use batch operations (slices/channels) for large inserts
**DON'T:**
- Load entire large result sets into memory when streaming is possible
- Use `SelectWrites()` as default (defeats read pool optimization)
- Cache everything (tune TTL based on access patterns)
## Advanced Patterns
### Streaming with Channels
**Select into channel:**
```go
userCh := make(chan User)
go func() {
defer close(userCh)
db.Select(userCh, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
}()
for user := range userCh {
// Process user
}
```
**Insert from channel:**
```go
userCh := make(chan User)
go func() {
for _, u := range users {
userCh <- u
}
close(userCh)
}()
err := db.Insert("users", userCh)
```
### Function Receivers
```go
err := db.Select(func(u User) {
log.Printf("Processing user: %s", u.Name)
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
```
### Transaction Management
```go
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel()
if err != nil {
return err
}
// Store transaction in context
ctx = mysql.NewContextWithTx(ctx, tx)
// Do database operations...
if err := commit(); err != nil {
return err
}
```
### Custom Interfaces
**Custom zero detection:**
```go
type CustomTime struct {
time.Time
}
func (ct CustomTime) IsZero() bool {
return ct.Time.IsZero() || ct.Time.Unix() == 0
}
```
**Custom value conversion:**
```go
type Point struct {
X, Y float64
}
func (p Point) Values() []any {
return []any{p.X, p.Y}
}
```
## Environment Variables
Configure behavior via environment variables:
- `COOL_MAX_EXECUTION_TIME_TIME` - Max query execution time (default: 27s)
- `COOL_MAX_ATTEMPTS` - Max retry attempts (default: unlimited)
- `COOL_REDIS_LOCK_RETRY_DELAY` - Lock retry delay (default: 0.020s)
- `COOL_MYSQL_MAX_QUERY_LOG_LENGTH` - Max query length in logs (default: 4096 bytes)
## Bundled Resources
This skill includes comprehensive reference documentation and working examples:
### Reference Documentation (`references/`)
- **api-reference.md** - Complete API documentation for all methods
- **query-patterns.md** - Query pattern examples and best practices
- **caching-guide.md** - Detailed caching strategies and configuration
- **struct-tags.md** - Comprehensive struct tag reference
- **testing-patterns.md** - Testing approaches with sqlmock
To access reference documentation:
```
Read references/api-reference.md for complete API documentation
Read references/query-patterns.md for query examples
Read references/caching-guide.md for caching strategies
Read references/struct-tags.md for struct tag details
Read references/testing-patterns.md for testing patterns
```
### Working Examples (`examples/`)
- **basic-crud.go** - Simple CRUD operations
- **advanced-queries.go** - Templates, channels, function receivers
- **caching-setup.go** - Cache configuration examples
- **transaction-patterns.go** - Transaction handling patterns
- **upsert-examples.go** - Upsert use cases
To access examples:
```
Read examples/basic-crud.go for basic patterns
Read examples/advanced-queries.go for advanced usage
Read examples/caching-setup.go for cache setup
Read examples/transaction-patterns.go for transactions
Read examples/upsert-examples.go for upsert patterns
```
## Common Gotchas
1. **Empty Result Handling**: Selecting into slice returns empty slice (not `sql.ErrNoRows`); selecting into single value returns `sql.ErrNoRows`
2. **Template vs Column Names**: Templates use field names (`.Name`), not column names from tags
3. **Cache Keys**: Include both query and parameters, so identical queries with different params cache separately
4. **Read/Write Consistency**: Use `SelectWrites()` immediately after writes, not `Select()`
5. **Struct Tag Priority**: `mysql` tag overrides field name for column mapping
6. **Parameter Case**: Parameters are case-insensitive when merged (normalized to lowercase)
7. **Automatic Chunking**: Large inserts automatically chunk based on `max_allowed_packet`
8. **Retry Behavior**: Automatic retries for error codes 1213 (deadlock), 1205 (lock timeout), 2006 (server gone), 2013 (connection lost)
## Next Steps
- Read `references/api-reference.md` for complete API documentation
- Check `examples/basic-crud.go` to see common patterns in action
- Review `references/caching-guide.md` for caching best practices
- Study `references/struct-tags.md` for advanced struct mapping
- Explore `examples/advanced-queries.go` for complex query patterns

View File

@@ -0,0 +1,725 @@
// Package examples demonstrates advanced query patterns with cool-mysql
package examples
import (
"encoding/json"
"fmt"
"html/template"
"log"
"strings"
"time"
mysql "github.com/StirlingMarketingGroup/cool-mysql"
)
// AdvancedQueryExamples demonstrates advanced query patterns
func AdvancedQueryExamples() {
db, err := setupDatabase()
if err != nil {
log.Fatalf("Failed to setup database: %v", err)
}
// Template queries
fmt.Println("=== TEMPLATE QUERY EXAMPLES ===")
templateExamples(db)
// Channel streaming
fmt.Println("\n=== CHANNEL STREAMING EXAMPLES ===")
channelExamples(db)
// Function receivers
fmt.Println("\n=== FUNCTION RECEIVER EXAMPLES ===")
functionReceiverExamples(db)
// JSON handling
fmt.Println("\n=== JSON HANDLING EXAMPLES ===")
jsonExamples(db)
// Raw SQL
fmt.Println("\n=== RAW SQL EXAMPLES ===")
rawSQLExamples(db)
// Complex queries
fmt.Println("\n=== COMPLEX QUERY EXAMPLES ===")
complexQueryExamples(db)
}
// templateExamples demonstrates Go template syntax in queries
func templateExamples(db *mysql.Database) {
// Example 1: Conditional WHERE clause
fmt.Println("1. Conditional WHERE clause")
type SearchParams struct {
MinAge int
Status string
Name string
}
// Search with all parameters
params := SearchParams{
MinAge: 25,
Status: "active",
Name: "Alice",
}
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
" WHERE 1=1" +
" {{ if .MinAge }}AND `age` >= @@MinAge{{ end }}" +
" {{ if .Status }}AND `status` = @@Status{{ end }}" +
" {{ if .Name }}AND `name` LIKE CONCAT('%', @@Name, '%'){{ end }}"
var users []User
err := db.Select(&users, query, 0, params)
if err != nil {
log.Printf("Template query failed: %v", err)
} else {
fmt.Printf("✓ Found %d users with filters\n", len(users))
}
// Example 2: Dynamic ORDER BY (with validation)
fmt.Println("\n2. Dynamic ORDER BY with whitelisting")
type SortParams struct {
SortBy string
SortOrder string
}
// Whitelist allowed columns - identifiers can't be marshaled
allowedColumns := map[string]bool{
"created_at": true,
"name": true,
"age": true,
}
sortParams := SortParams{
SortBy: "created_at",
SortOrder: "DESC",
}
// Validate before using in query
if !allowedColumns[sortParams.SortBy] {
log.Printf("Invalid sort column: %s", sortParams.SortBy)
return
}
sortQuery := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
" WHERE `active` = 1" +
" {{ if .SortBy }}" +
" ORDER BY {{ .SortBy }} {{ .SortOrder }}" +
" {{ end }}"
err = db.Select(&users, sortQuery, 0, sortParams)
if err != nil {
log.Printf("Sort query failed: %v", err)
} else {
fmt.Printf("✓ Users sorted by %s %s\n", sortParams.SortBy, sortParams.SortOrder)
}
// Example 3: Conditional JOINs
fmt.Println("\n3. Conditional JOINs")
type JoinParams struct {
IncludeOrders bool
IncludeAddress bool
IncludeMetadata bool
}
joinParams := JoinParams{
IncludeOrders: true,
IncludeAddress: false,
}
joinQuery := "SELECT `users`.`id`, `users`.`name`, `users`.`email`, `users`.`age`, `users`.`active`, `users`.`created_at`, `users`.`updated_at`" +
" {{ if .IncludeOrders }}, COUNT(`orders`.`id`) as `order_count`{{ end }}" +
" {{ if .IncludeAddress }}, `addresses`.`city`{{ end }}" +
" FROM `users`" +
" {{ if .IncludeOrders }}" +
" LEFT JOIN `orders` ON `users`.`id` = `orders`.`user_id`" +
" {{ end }}" +
" {{ if .IncludeAddress }}" +
" LEFT JOIN `addresses` ON `users`.`id` = `addresses`.`user_id`" +
" {{ end }}" +
" GROUP BY `users`.`id`"
err = db.Select(&users, joinQuery, 0, joinParams)
if err != nil {
log.Printf("Join query failed: %v", err)
} else {
fmt.Println("✓ Query with conditional joins executed")
}
// Example 4: Custom template functions
fmt.Println("\n4. Custom template functions")
// Add custom functions
db.AddTemplateFuncs(template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
"quote": func(s string) string { return fmt.Sprintf("'%s'", s) },
})
type CaseParams struct {
SearchTerm string
CaseSensitive bool
UseWildcard bool
}
caseParams := CaseParams{
SearchTerm: "alice",
CaseSensitive: false,
UseWildcard: true,
}
// IMPORTANT: Template values must be marshaled with | marshal
caseQuery := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
" WHERE {{ if not .CaseSensitive }}UPPER(`name`){{ else }}`name`{{ end }} LIKE CONCAT('%', {{ .SearchTerm | marshal }}, '%')"
err = db.Select(&users, caseQuery, 0, caseParams)
if err != nil {
log.Printf("Custom function query failed: %v", err)
} else {
fmt.Println("✓ Query with custom template functions executed")
}
// Example 5: Complex conditional logic
fmt.Println("\n5. Complex conditional logic")
type FilterParams struct {
AgeRange []int
Statuses []string
DateFrom time.Time
DateTo time.Time
ActiveOnly bool
}
filterParams := FilterParams{
AgeRange: []int{25, 40},
ActiveOnly: true,
DateFrom: time.Now().Add(-30 * 24 * time.Hour),
}
filterQuery := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
" WHERE 1=1" +
" {{ if .AgeRange }}" +
" AND `age` BETWEEN @@AgeMin AND @@AgeMax" +
" {{ end }}" +
" {{ if .ActiveOnly }}" +
" AND `active` = 1" +
" {{ end }}" +
" {{ if not .DateFrom.IsZero }}" +
" AND `created_at` >= @@DateFrom" +
" {{ end }}" +
" {{ if not .DateTo.IsZero }}" +
" AND `created_at` <= @@DateTo" +
" {{ end }}"
queryParams := mysql.Params{
"AgeMin": filterParams.AgeRange[0],
"AgeMax": filterParams.AgeRange[1],
"DateFrom": filterParams.DateFrom,
}
err = db.Select(&users, filterQuery, 0, queryParams, filterParams)
if err != nil {
log.Printf("Complex filter query failed: %v", err)
} else {
fmt.Printf("✓ Found %d users with complex filters\n", len(users))
}
}
// channelExamples demonstrates streaming with channels
func channelExamples(db *mysql.Database) {
// Example 1: Stream SELECT results
fmt.Println("1. Stream SELECT results to channel")
userCh := make(chan User, 10) // Buffered channel
go func() {
defer close(userCh)
err := db.Select(userCh, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = 1", 0)
if err != nil {
log.Printf("Channel select failed: %v", err)
}
}()
count := 0
for user := range userCh {
fmt.Printf(" Received: %s (%s)\n", user.Name, user.Email)
count++
if count >= 5 {
fmt.Println(" (showing first 5 results)")
// Drain remaining
for range userCh {
}
break
}
}
// Example 2: Stream INSERT from channel
fmt.Println("\n2. Stream INSERT from channel")
insertCh := make(chan User, 10)
go func() {
defer close(insertCh)
for i := 0; i < 100; i++ {
insertCh <- User{
Name: fmt.Sprintf("StreamUser%d", i),
Email: fmt.Sprintf("stream%d@example.com", i),
Age: 20 + (i % 50),
Active: i%2 == 0,
}
}
}()
err := db.Insert("users", insertCh)
if err != nil {
log.Printf("Channel insert failed: %v", err)
} else {
fmt.Println("✓ Streamed 100 users for insertion")
}
// Example 3: Transform while streaming
fmt.Println("\n3. Transform data while streaming")
type EnrichedUser struct {
User
Category string
Priority int
}
rawUserCh := make(chan User, 10)
enrichedCh := make(chan EnrichedUser, 10)
// Producer: fetch users
go func() {
defer close(rawUserCh)
db.Select(rawUserCh, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` LIMIT @@limit", 0, 50)
}()
// Transformer: enrich data
go func() {
defer close(enrichedCh)
for user := range rawUserCh {
enriched := EnrichedUser{
User: user,
}
// Add category based on age
if user.Age < 25 {
enriched.Category = "Young"
enriched.Priority = 1
} else if user.Age < 40 {
enriched.Category = "Middle"
enriched.Priority = 2
} else {
enriched.Category = "Senior"
enriched.Priority = 3
}
enrichedCh <- enriched
}
}()
// Consumer: process enriched data
processed := 0
for enriched := range enrichedCh {
processed++
_ = enriched // Process enriched user
}
fmt.Printf("✓ Processed %d enriched users\n", processed)
}
// functionReceiverExamples demonstrates function receivers
func functionReceiverExamples(db *mysql.Database) {
// Example 1: Process each row with function
fmt.Println("1. Process rows with function")
count := 0
err := db.Select(func(u User) {
count++
if count <= 3 {
fmt.Printf(" Processing: %s (Age: %d)\n", u.Name, u.Age)
}
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = 1", 0)
if err != nil {
log.Printf("Function receiver failed: %v", err)
} else {
fmt.Printf("✓ Processed %d users\n", count)
}
// Example 2: Aggregate data with function
fmt.Println("\n2. Aggregate data with function")
var totalAge int
var userCount int
err = db.Select(func(u User) {
totalAge += u.Age
userCount++
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
if err != nil {
log.Printf("Aggregation failed: %v", err)
} else if userCount > 0 {
avgAge := float64(totalAge) / float64(userCount)
fmt.Printf("✓ Average age: %.2f (%d users)\n", avgAge, userCount)
}
// Example 3: Conditional processing
fmt.Println("\n3. Conditional processing with function")
type Stats struct {
YoungCount int
MiddleCount int
SeniorCount int
}
stats := Stats{}
err = db.Select(func(u User) {
switch {
case u.Age < 25:
stats.YoungCount++
case u.Age < 40:
stats.MiddleCount++
default:
stats.SeniorCount++
}
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
if err != nil {
log.Printf("Stats failed: %v", err)
} else {
fmt.Printf("✓ Age distribution: Young=%d, Middle=%d, Senior=%d\n",
stats.YoungCount, stats.MiddleCount, stats.SeniorCount)
}
// Example 4: Early termination pattern
fmt.Println("\n4. Early termination with function")
found := false
targetEmail := "alice@example.com"
err = db.Select(func(u User) {
if u.Email == targetEmail {
found = true
fmt.Printf("✓ Found user: %s\n", u.Name)
// Note: Can't actually stop iteration early
// This is a limitation of function receivers
}
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
if err != nil {
log.Printf("Search failed: %v", err)
} else if !found {
fmt.Println("✗ User not found")
}
}
// jsonExamples demonstrates JSON handling
func jsonExamples(db *mysql.Database) {
// Example 1: Store JSON in struct field
fmt.Println("1. Store JSON column in struct")
type UserWithMeta struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Email string `mysql:"email"`
Metadata json.RawMessage `mysql:"metadata"` // JSON column
}
userMeta := UserWithMeta{
Name: "JSONUser",
Email: "json@example.com",
Metadata: json.RawMessage(`{
"theme": "dark",
"language": "en",
"notifications": true
}`),
}
err := db.Insert("users", userMeta)
if err != nil {
log.Printf("JSON insert failed: %v", err)
} else {
fmt.Println("✓ User with JSON metadata inserted")
}
// Example 2: Select JSON as RawMessage
fmt.Println("\n2. Select JSON column")
var usersWithMeta []UserWithMeta
err = db.Select(&usersWithMeta,
"SELECT `id`, `name`, `email`, metadata FROM `users` WHERE metadata IS NOT NULL LIMIT @@limit",
0,
5)
if err != nil {
log.Printf("JSON select failed: %v", err)
} else {
fmt.Printf("✓ Retrieved %d users with metadata\n", len(usersWithMeta))
for _, u := range usersWithMeta {
fmt.Printf(" %s: %s\n", u.Name, string(u.Metadata))
}
}
// Example 3: SelectJSON for JSON result
fmt.Println("\n3. SelectJSON for JSON object")
var jsonResult json.RawMessage
err = db.SelectJSON(&jsonResult,
"SELECT JSON_OBJECT("+
" 'id', `id`,"+
" 'name', `name`,"+
" 'email', `email`,"+
" 'age', `age`"+
" ) FROM `users` WHERE `id` = @@id",
0,
1)
if err != nil {
log.Printf("SelectJSON failed: %v", err)
} else {
fmt.Printf("✓ JSON result: %s\n", string(jsonResult))
}
// Example 4: SelectJSON for JSON array
fmt.Println("\n4. SelectJSON for JSON array")
var jsonArray json.RawMessage
err = db.SelectJSON(&jsonArray,
"SELECT JSON_ARRAYAGG("+
" JSON_OBJECT("+
" 'name', `name`,"+
" 'email', `email`"+
" )"+
" ) FROM `users` WHERE `active` = 1 LIMIT @@limit",
0,
5)
if err != nil {
log.Printf("SelectJSON array failed: %v", err)
} else {
fmt.Printf("✓ JSON array result: %s\n", string(jsonArray))
}
}
// rawSQLExamples demonstrates using mysql.Raw for literal SQL
func rawSQLExamples(db *mysql.Database) {
// Example 1: Raw SQL in WHERE clause
fmt.Println("1. Raw SQL for complex condition")
var users []User
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE @@condition",
0,
mysql.Raw("created_at > NOW() - INTERVAL 7 DAY"))
if err != nil {
log.Printf("Raw SQL query failed: %v", err)
} else {
fmt.Printf("✓ Found %d users from last 7 days\n", len(users))
}
// Example 2: Raw SQL for CASE statement
fmt.Println("\n2. Raw SQL for CASE statement")
type UserWithLabel struct {
Name string `mysql:"name"`
Label string `mysql:"label"`
}
caseSQL := mysql.Raw(`
CASE
WHEN age < 25 THEN 'Young'
WHEN age < 40 THEN 'Middle'
ELSE 'Senior'
END
`)
var labeled []UserWithLabel
err = db.Select(&labeled,
"SELECT name, @@ageCase as `label` FROM `users`",
0,
caseSQL)
if err != nil {
log.Printf("CASE query failed: %v", err)
} else {
fmt.Printf("✓ Retrieved %d users with age labels\n", len(labeled))
for i, u := range labeled {
if i < 3 {
fmt.Printf(" %s: %s\n", u.Name, u.Label)
}
}
}
// Example 3: Raw SQL for subquery
fmt.Println("\n3. Raw SQL for subquery")
subquery := mysql.Raw("(SELECT AVG(age) FROM `users` WHERE `active` = 1)")
err = db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@avgAge",
0,
subquery)
if err != nil {
log.Printf("Subquery failed: %v", err)
} else {
fmt.Printf("✓ Found %d users above average age\n", len(users))
}
// Example 4: WARNING - Never use Raw with user input!
fmt.Println("\n4. WARNING: Raw SQL security example")
// DANGEROUS - SQL injection risk!
// userInput := "'; DROP TABLE users; --"
// db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `name` = @@name", 0,
// mysql.Params{"name": mysql.Raw(userInput)})
// SAFE - use regular parameter
safeInput := "Alice'; DROP TABLE users; --"
err = db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `name` = @@name", 0, safeInput) // Properly escaped
if err != nil {
log.Printf("Safe query failed: %v", err)
} else {
fmt.Println("✓ User input safely escaped (no SQL injection)")
}
}
// complexQueryExamples demonstrates complex query patterns
func complexQueryExamples(db *mysql.Database) {
// Example 1: Subquery with named parameters
fmt.Println("1. Subquery with parameters")
type UserWithOrderCount struct {
User
OrderCount int `mysql:"order_count"`
}
var usersWithOrders []UserWithOrderCount
err := db.Select(&usersWithOrders,
"SELECT `users`.`id`, `users`.`name`, `users`.`email`, `users`.`age`, `users`.`active`, `users`.`created_at`, `users`.`updated_at`,"+
" (SELECT COUNT(*) FROM `orders` WHERE `orders`.`user_id` = `users`.`id`) as `order_count`"+
" FROM `users`"+
" WHERE `users`.`created_at` > @@since"+
" AND `users`.`active` = @@active",
5*time.Minute,
mysql.Params{
"since": time.Now().Add(-30 * 24 * time.Hour),
"active": true,
})
if err != nil {
log.Printf("Subquery failed: %v", err)
} else {
fmt.Printf("✓ Retrieved %d active users with order counts\n", len(usersWithOrders))
}
// Example 2: JOIN with aggregation
fmt.Println("\n2. JOIN with aggregation")
query := "SELECT" +
" `users`.`id`," +
" `users`.`name`," +
" `users`.`email`," +
" COUNT(`orders`.`id`) as `order_count`," +
" SUM(`orders`.`total`) as `total_spent`" +
" FROM `users`" +
" LEFT JOIN `orders` ON `users`.`id` = `orders`.`user_id`" +
" WHERE `users`.`active` = @@active" +
" GROUP BY `users`.`id`, `users`.`name`, `users`.`email`" +
" HAVING COUNT(`orders`.`id`) > @@minOrders" +
" ORDER BY total_spent DESC" +
" LIMIT @@limit"
type UserStats struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Email string `mysql:"email"`
OrderCount int `mysql:"order_count"`
TotalSpent float64 `mysql:"total_spent"`
}
var stats []UserStats
err = db.Select(&stats, query, 10*time.Minute,
mysql.Params{
"active": true,
"minOrders": 5,
"limit": 10,
})
if err != nil {
log.Printf("Aggregation query failed: %v", err)
} else {
fmt.Printf("✓ Top %d spenders retrieved\n", len(stats))
}
// Example 3: Window function
fmt.Println("\n3. Window function query")
windowQuery := "SELECT" +
" `id`," +
" `name`," +
" `age`," +
" RANK() OVER (ORDER BY `age` DESC) as `age_rank`," +
" AVG(`age`) OVER () as `avg_age`" +
" FROM `users`" +
" WHERE `active` = @@active" +
" LIMIT @@limit"
type UserWithRank struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Age int `mysql:"age"`
AgeRank int `mysql:"age_rank"`
AvgAge float64 `mysql:"avg_age"`
}
var ranked []UserWithRank
err = db.Select(&ranked, windowQuery, 5*time.Minute,
mysql.Params{
"active": true,
"limit": 20,
})
if err != nil {
log.Printf("Window function query failed: %v", err)
} else {
fmt.Printf("✓ Retrieved %d users with age ranking\n", len(ranked))
}
// Example 4: CTE (Common Table Expression)
fmt.Println("\n4. CTE query")
cteQuery := "WITH recent_users AS (" +
" SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`" +
" WHERE `created_at` > @@since" +
" )," +
" active_recent AS (" +
" SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM recent_users" +
" WHERE `active` = @@active" +
" )" +
" SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM active_recent" +
" ORDER BY `created_at` DESC" +
" LIMIT @@limit"
var cteUsers []User
err = db.Select(&cteUsers, cteQuery, 5*time.Minute,
mysql.Params{
"since": time.Now().Add(-7 * 24 * time.Hour),
"active": true,
"limit": 10,
})
if err != nil {
log.Printf("CTE query failed: %v", err)
} else {
fmt.Printf("✓ Retrieved %d recent active users via CTE\n", len(cteUsers))
}
}

517
examples/basic-crud.go Normal file
View File

@@ -0,0 +1,517 @@
// Package examples demonstrates basic CRUD operations with cool-mysql
package examples
import (
"context"
"database/sql"
"errors"
"fmt"
"log"
"time"
mysql "github.com/StirlingMarketingGroup/cool-mysql"
)
// User represents a user in the database
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Email string `mysql:"email"`
Age int `mysql:"age"`
Active bool `mysql:"active"`
CreatedAt time.Time `mysql:"created_at,defaultzero"`
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
}
// BasicCRUDExamples demonstrates basic Create, Read, Update, Delete operations
func BasicCRUDExamples() {
// Setup database connection
db, err := setupDatabase()
if err != nil {
log.Fatalf("Failed to setup database: %v", err)
}
// Create
fmt.Println("=== CREATE EXAMPLES ===")
createExamples(db)
// Read
fmt.Println("\n=== READ EXAMPLES ===")
readExamples(db)
// Update
fmt.Println("\n=== UPDATE EXAMPLES ===")
updateExamples(db)
// Delete
fmt.Println("\n=== DELETE EXAMPLES ===")
deleteExamples(db)
// Utility queries
fmt.Println("\n=== UTILITY EXAMPLES ===")
utilityExamples(db)
}
// setupDatabase creates a connection to MySQL
func setupDatabase() (*mysql.Database, error) {
// Create database connection with read/write pools
db, err := mysql.New(
"root", // write user
"password", // write password
"mydb", // write schema
"localhost", // write host
3306, // write port
"root", // read user
"password", // read password
"mydb", // read schema
"localhost", // read host
3306, // read port
"utf8mb4_unicode_ci", // collation
time.UTC, // timezone
)
if err != nil {
return nil, fmt.Errorf("failed to create database: %w", err)
}
return db, nil
}
// createExamples demonstrates INSERT operations
func createExamples(db *mysql.Database) {
// Example 1: Insert single user
fmt.Println("1. Insert single user")
user := User{
Name: "Alice",
Email: "alice@example.com",
Age: 25,
Active: true,
}
err := db.Insert("users", user)
if err != nil {
log.Printf("Insert failed: %v", err)
} else {
fmt.Println("✓ User inserted successfully")
}
// Example 2: Insert with explicit zero values
fmt.Println("\n2. Insert with timestamp defaults")
userWithDefaults := User{
Name: "Bob",
Email: "bob@example.com",
Age: 30,
Active: true,
// CreatedAt and UpdatedAt are zero values
// With ,defaultzero tag, database will use DEFAULT values
}
err = db.Insert("users", userWithDefaults)
if err != nil {
log.Printf("Insert failed: %v", err)
} else {
fmt.Println("✓ User inserted with database defaults")
}
// Example 3: Batch insert
fmt.Println("\n3. Batch insert multiple users")
users := []User{
{Name: "Charlie", Email: "charlie@example.com", Age: 28, Active: true},
{Name: "Diana", Email: "diana@example.com", Age: 32, Active: true},
{Name: "Eve", Email: "eve@example.com", Age: 27, Active: false},
}
err = db.Insert("users", users)
if err != nil {
log.Printf("Batch insert failed: %v", err)
} else {
fmt.Printf("✓ Batch inserted %d users\n", len(users))
}
// Example 4: Insert with context
fmt.Println("\n4. Insert with context timeout")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
contextUser := User{
Name: "Frank",
Email: "frank@example.com",
Age: 35,
Active: true,
}
err = db.InsertContext(ctx, "users", contextUser)
if err != nil {
log.Printf("Insert with context failed: %v", err)
} else {
fmt.Println("✓ User inserted with context")
}
}
// readExamples demonstrates SELECT operations
func readExamples(db *mysql.Database) {
// Example 1: Select all users into slice
fmt.Println("1. Select all users")
var allUsers []User
err := db.Select(&allUsers, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
if err != nil {
log.Printf("Select all failed: %v", err)
} else {
fmt.Printf("✓ Found %d users\n", len(allUsers))
for _, u := range allUsers {
fmt.Printf(" - %s (%s), Age: %d\n", u.Name, u.Email, u.Age)
}
}
// Example 2: Select with named parameters
fmt.Println("\n2. Select users with age filter")
var adults []User
err = db.Select(&adults,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age >= @@minAge",
0, // No caching
25)
if err != nil {
log.Printf("Select with filter failed: %v", err)
} else {
fmt.Printf("✓ Found %d users aged 25+\n", len(adults))
}
// Example 3: Select single user
fmt.Println("\n3. Select single user by email")
var user User
err = db.Select(&user,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
0,
"alice@example.com")
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("✗ User not found")
} else if err != nil {
log.Printf("Select failed: %v", err)
} else {
fmt.Printf("✓ Found user: %s (ID: %d)\n", user.Name, user.ID)
}
// Example 4: Select single value
fmt.Println("\n4. Select single value (name)")
var name string
err = db.Select(&name,
"SELECT `name` FROM `users` WHERE `email` = @@email",
0,
"bob@example.com")
if err != nil {
log.Printf("Select name failed: %v", err)
} else {
fmt.Printf("✓ User name: %s\n", name)
}
// Example 5: Select with multiple conditions
fmt.Println("\n5. Select with multiple conditions")
var activeAdults []User
err = db.Select(&activeAdults,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`"+
" WHERE `age` >= @@minAge"+
" AND `active` = @@active"+
" ORDER BY `name`",
0,
mysql.Params{
"minAge": 25,
"active": true,
})
if err != nil {
log.Printf("Select failed: %v", err)
} else {
fmt.Printf("✓ Found %d active adult users\n", len(activeAdults))
}
// Example 6: Select with caching
fmt.Println("\n6. Select with caching (5 minute TTL)")
var cachedUsers []User
err = db.Select(&cachedUsers,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
5*time.Minute, // Cache for 5 minutes
true)
if err != nil {
log.Printf("Cached select failed: %v", err)
} else {
fmt.Printf("✓ Found %d active users (cached)\n", len(cachedUsers))
}
// Example 7: Select with context
fmt.Println("\n7. Select with context timeout")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var contextUsers []User
err = db.SelectContext(ctx, &contextUsers,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` LIMIT @@limit",
0,
10)
if err != nil {
log.Printf("Select with context failed: %v", err)
} else {
fmt.Printf("✓ Found %d users with context\n", len(contextUsers))
}
}
// updateExamples demonstrates UPDATE operations
func updateExamples(db *mysql.Database) {
// Example 1: Simple update
fmt.Println("1. Update user name")
err := db.Exec(
"UPDATE `users` SET `name` = @@name WHERE `email` = @@email",
mysql.Params{
"name": "Alice Smith",
"email": "alice@example.com",
})
if err != nil {
log.Printf("Update failed: %v", err)
} else {
fmt.Println("✓ User name updated")
}
// Example 2: Update with result
fmt.Println("\n2. Update with result check")
result, err := db.ExecResult(
"UPDATE `users` SET `age` = @@age WHERE `email` = @@email",
mysql.Params{
"age": 26,
"email": "alice@example.com",
})
if err != nil {
log.Printf("Update failed: %v", err)
} else {
rowsAffected, _ := result.RowsAffected()
fmt.Printf("✓ Updated %d row(s)\n", rowsAffected)
}
// Example 3: Update multiple rows
fmt.Println("\n3. Update multiple rows")
result, err = db.ExecResult(
"UPDATE `users` SET `active` = @@active WHERE age < @@maxAge",
mysql.Params{
"active": false,
"maxAge": 25,
})
if err != nil {
log.Printf("Bulk update failed: %v", err)
} else {
rowsAffected, _ := result.RowsAffected()
fmt.Printf("✓ Deactivated %d user(s)\n", rowsAffected)
}
// Example 4: Update with current timestamp
fmt.Println("\n4. Update timestamp")
err = db.Exec(
"UPDATE `users` SET `updated_at` = NOW() WHERE `email` = @@email",
"bob@example.com")
if err != nil {
log.Printf("Update timestamp failed: %v", err)
} else {
fmt.Println("✓ Timestamp updated")
}
// Example 5: Conditional update
fmt.Println("\n5. Conditional update (only if age is current value)")
err = db.Exec(
"UPDATE `users`"+
" SET `age` = @@newAge"+
" WHERE `email` = @@email"+
" AND `age` = @@currentAge",
mysql.Params{
"newAge": 27,
"email": "charlie@example.com",
"currentAge": 28,
})
if err != nil {
log.Printf("Conditional update failed: %v", err)
} else {
fmt.Println("✓ Conditional update executed")
}
// Example 6: Update with context
fmt.Println("\n6. Update with context")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = db.ExecContext(ctx,
"UPDATE `users` SET `active` = @@active WHERE age > @@age",
mysql.Params{
"active": true,
"age": 30,
})
if err != nil {
log.Printf("Update with context failed: %v", err)
} else {
fmt.Println("✓ Update executed with context")
}
}
// deleteExamples demonstrates DELETE operations
func deleteExamples(db *mysql.Database) {
// Example 1: Delete single record
fmt.Println("1. Delete single user")
err := db.Exec(
"DELETE FROM `users` WHERE `email` = @@email",
"eve@example.com")
if err != nil {
log.Printf("Delete failed: %v", err)
} else {
fmt.Println("✓ User deleted")
}
// Example 2: Delete with result check
fmt.Println("\n2. Delete with result check")
result, err := db.ExecResult(
"DELETE FROM `users` WHERE `email` = @@email",
"frank@example.com")
if err != nil {
log.Printf("Delete failed: %v", err)
} else {
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
fmt.Println("✗ No user found to delete")
} else {
fmt.Printf("✓ Deleted %d user(s)\n", rowsAffected)
}
}
// Example 3: Delete multiple records
fmt.Println("\n3. Delete inactive users")
result, err = db.ExecResult(
"DELETE FROM `users` WHERE `active` = @@active",
false)
if err != nil {
log.Printf("Bulk delete failed: %v", err)
} else {
rowsAffected, _ := result.RowsAffected()
fmt.Printf("✓ Deleted %d inactive user(s)\n", rowsAffected)
}
// Example 4: Delete with age condition
fmt.Println("\n4. Delete users under age threshold")
result, err = db.ExecResult(
"DELETE FROM `users` WHERE age < @@minAge",
18)
if err != nil {
log.Printf("Delete failed: %v", err)
} else {
rowsAffected, _ := result.RowsAffected()
fmt.Printf("✓ Deleted %d user(s) under 18\n", rowsAffected)
}
// Example 5: Delete with context
fmt.Println("\n5. Delete with context")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = db.ExecContext(ctx,
"DELETE FROM `users` WHERE `created_at` < @@cutoff",
time.Now().Add(-365*24*time.Hour))
if err != nil {
log.Printf("Delete with context failed: %v", err)
} else {
fmt.Println("✓ Old users deleted with context")
}
}
// utilityExamples demonstrates utility query methods
func utilityExamples(db *mysql.Database) {
// Example 1: Count users
fmt.Println("1. Count all users")
count, err := db.Count(
"SELECT COUNT(*) FROM `users`",
0) // No caching
if err != nil {
log.Printf("Count failed: %v", err)
} else {
fmt.Printf("✓ Total users: %d\n", count)
}
// Example 2: Count with condition
fmt.Println("\n2. Count active users")
activeCount, err := db.Count(
"SELECT COUNT(*) FROM `users` WHERE `active` = @@active",
0,
true)
if err != nil {
log.Printf("Count failed: %v", err)
} else {
fmt.Printf("✓ Active users: %d\n", activeCount)
}
// Example 3: Count with caching
fmt.Println("\n3. Count with caching")
cachedCount, err := db.Count(
"SELECT COUNT(*) FROM `users`",
5*time.Minute) // Cache for 5 minutes
if err != nil {
log.Printf("Cached count failed: %v", err)
} else {
fmt.Printf("✓ Cached user count: %d\n", cachedCount)
}
// Example 4: Check if user exists
fmt.Println("\n4. Check if email exists")
exists, err := db.Exists(
"SELECT 1 FROM `users` WHERE `email` = @@email",
0,
"alice@example.com")
if err != nil {
log.Printf("Exists check failed: %v", err)
} else {
if exists {
fmt.Println("✓ Email exists in database")
} else {
fmt.Println("✗ Email not found")
}
}
// Example 5: Check existence with multiple conditions
fmt.Println("\n5. Check if active adult exists")
exists, err = db.Exists(
"SELECT 1 FROM `users`"+
" WHERE `active` = @@active"+
" AND `age` >= @@minAge",
0,
mysql.Params{
"active": true,
"minAge": 25,
})
if err != nil {
log.Printf("Exists check failed: %v", err)
} else {
if exists {
fmt.Println("✓ Active adult user exists")
} else {
fmt.Println("✗ No active adult users found")
}
}
// Example 6: Read-after-write with SelectWrites
fmt.Println("\n6. Read-after-write consistency")
// Insert user
newUser := User{
Name: "Grace",
Email: "grace@example.com",
Age: 29,
Active: true,
}
err = db.Insert("users", newUser)
if err != nil {
log.Printf("Insert failed: %v", err)
return
}
// Immediately read using write pool for consistency
var retrieved User
err = db.SelectWrites(&retrieved,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
0, // Don't cache writes
"grace@example.com")
if err != nil {
log.Printf("SelectWrites failed: %v", err)
} else {
fmt.Printf("✓ User retrieved immediately after insert: %s (ID: %d)\n",
retrieved.Name, retrieved.ID)
}
}

534
examples/caching-setup.go Normal file
View File

@@ -0,0 +1,534 @@
// Package examples demonstrates caching configuration with cool-mysql
package examples
import (
"context"
"fmt"
"log"
"time"
"github.com/bradfitz/gomemcache/memcache"
"github.com/redis/go-redis/v9"
mysql "github.com/StirlingMarketingGroup/cool-mysql"
)
// CachingExamples demonstrates various caching setups and strategies
func CachingExamples() {
fmt.Println("=== CACHING SETUP EXAMPLES ===")
// In-memory caching
fmt.Println("\n1. In-Memory Weak Cache")
weakCacheExample()
// Redis caching
fmt.Println("\n2. Redis Cache")
redisCacheExample()
// Redis Cluster caching
fmt.Println("\n3. Redis Cluster Cache")
redisClusterExample()
// Memcached caching
fmt.Println("\n4. Memcached Cache")
memcachedCacheExample()
// Multi-level caching
fmt.Println("\n5. Multi-Level Cache")
multiCacheExample()
// Cache strategies
fmt.Println("\n6. Cache Strategies")
cacheStrategiesExample()
// Performance benchmark
fmt.Println("\n7. Performance Benchmark")
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
performanceBenchmark(db)
// Cache key debugging
fmt.Println("\n8. Cache Key Debugging")
cacheKeyDebug(db)
}
// weakCacheExample demonstrates in-memory weak cache
func weakCacheExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
// Enable in-memory weak cache
db.UseCache(mysql.NewWeakCache())
fmt.Println("✓ Weak cache enabled (GC-managed, local only)")
// First query - cache miss
start := time.Now()
var users []User
err = db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
5*time.Minute, // Cache for 5 minutes
true)
duration1 := time.Since(start)
if err != nil {
log.Printf("First query failed: %v", err)
return
}
fmt.Printf(" First query (cache miss): %v, %d users\n", duration1, len(users))
// Second query - cache hit
start = time.Now()
err = db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
5*time.Minute,
true)
duration2 := time.Since(start)
if err != nil {
log.Printf("Second query failed: %v", err)
return
}
fmt.Printf(" Second query (cache hit): %v, %d users\n", duration2, len(users))
fmt.Printf(" Speedup: %.2fx faster\n", float64(duration1)/float64(duration2))
}
// redisCacheExample demonstrates Redis cache setup
func redisCacheExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
// Setup Redis client
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password
DB: 0, // default DB
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
PoolSize: 10,
MinIdleConns: 5,
})
// Test Redis connection
ctx := context.Background()
_, err = redisClient.Ping(ctx).Result()
if err != nil {
log.Printf("Redis connection failed: %v", err)
log.Println(" Skipping Redis cache example")
return
}
// Enable Redis cache
db.EnableRedis(redisClient)
fmt.Println("✓ Redis cache enabled (distributed, with locking)")
// Query with caching
var users []User
err = db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
10*time.Minute, // Cache for 10 minutes
18)
if err != nil {
log.Printf("Redis cached query failed: %v", err)
return
}
fmt.Printf(" Cached %d users in Redis\n", len(users))
fmt.Println(" ✓ Cache shared across all application instances")
fmt.Println(" ✓ Distributed locking prevents cache stampedes")
}
// redisClusterExample demonstrates Redis Cluster setup
func redisClusterExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
// Setup Redis Cluster client
redisCluster := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{
"localhost:7000",
"localhost:7001",
"localhost:7002",
},
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
PoolSize: 10,
})
// Test cluster connection
ctx := context.Background()
_, err = redisCluster.Ping(ctx).Result()
if err != nil {
log.Printf("Redis cluster connection failed: %v", err)
log.Println(" Skipping Redis cluster example")
return
}
// Enable Redis cluster cache
// Note: EnableRedis works with both single-node and cluster
db.EnableRedis(redisCluster)
fmt.Println("✓ Redis Cluster cache enabled")
// Query with caching
var users []User
err = db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` LIMIT @@limit", 5*time.Minute, 100)
if err != nil {
log.Printf("Cluster cached query failed: %v", err)
return
}
fmt.Printf(" Cached %d users in Redis Cluster\n", len(users))
}
// memcachedCacheExample demonstrates Memcached cache setup
func memcachedCacheExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
// Setup Memcached client
memcacheClient := memcache.New("localhost:11211")
memcacheClient.Timeout = 3 * time.Second
memcacheClient.MaxIdleConns = 10
// Test Memcached connection
err = memcacheClient.Ping()
if err != nil {
log.Printf("Memcached connection failed: %v", err)
log.Println(" Skipping Memcached example")
return
}
// Enable Memcached
db.EnableMemcache(memcacheClient)
fmt.Println("✓ Memcached cache enabled (distributed, simple)")
// Query with caching
var users []User
err = db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
15*time.Minute, // Cache for 15 minutes
true)
if err != nil {
log.Printf("Memcached query failed: %v", err)
return
}
fmt.Printf(" Cached %d users in Memcached\n", len(users))
fmt.Println(" ⚠ No distributed locking (potential cache stampedes)")
}
// multiCacheExample demonstrates multi-level caching
func multiCacheExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
// Setup Redis
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
ctx := context.Background()
_, err = redisClient.Ping(ctx).Result()
if err != nil {
log.Printf("Redis unavailable, using weak cache only")
db.UseCache(mysql.NewWeakCache())
return
}
// Create multi-level cache
// L1: Fast local weak cache
// L2: Shared Redis cache
multiCache := mysql.NewMultiCache(
mysql.NewWeakCache(), // L1: In-memory
mysql.NewRedisCache(redisClient), // L2: Redis
)
db.UseCache(multiCache)
fmt.Println("✓ Multi-level cache enabled")
fmt.Println(" L1: In-memory weak cache (fastest)")
fmt.Println(" L2: Redis distributed cache (shared)")
// First query - cold cache (misses both levels)
start := time.Now()
var users []User
err = db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
10*time.Minute,
21)
cold := time.Since(start)
if err != nil {
log.Printf("Cold cache query failed: %v", err)
return
}
fmt.Printf("\n Cold cache (DB query): %v, %d users\n", cold, len(users))
// Second query - warm cache (hits L1)
start = time.Now()
err = db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
10*time.Minute,
21)
warm := time.Since(start)
if err != nil {
log.Printf("Warm cache query failed: %v", err)
return
}
fmt.Printf(" Warm cache (L1 hit): %v\n", warm)
fmt.Printf(" Speedup: %.2fx faster\n", float64(cold)/float64(warm))
}
// cacheStrategiesExample demonstrates different caching strategies
func cacheStrategiesExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
db.UseCache(mysql.NewWeakCache())
// Strategy 1: No caching for real-time data
fmt.Println("\nStrategy 1: No caching (TTL = 0)")
var liveUsers []User
err = db.Select(&liveUsers,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `last_active` > @@since",
0, // No caching
time.Now().Add(-5*time.Minute))
if err != nil {
log.Printf("Live query failed: %v", err)
} else {
fmt.Printf(" ✓ %d active users (always fresh)\n", len(liveUsers))
}
// Strategy 2: Short TTL for frequently changing data
fmt.Println("\nStrategy 2: Short TTL (30 seconds)")
err = db.Select(&liveUsers,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active",
30*time.Second, // Short TTL
true)
if err != nil {
log.Printf("Short TTL query failed: %v", err)
} else {
fmt.Println(" ✓ Balance freshness and performance")
}
// Strategy 3: Long TTL for reference data
fmt.Println("\nStrategy 3: Long TTL (1 hour)")
type Country struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Code string `mysql:"code"`
}
var countries []Country
err = db.Select(&countries,
"SELECT `id`, `name`, `code` FROM `countries`",
time.Hour, // Long TTL for reference data
)
if err != nil {
log.Printf("Long TTL query failed: %v", err)
} else {
fmt.Printf(" ✓ %d countries (rarely changes)\n", len(countries))
}
// Strategy 4: Conditional caching based on result size
fmt.Println("\nStrategy 4: Conditional caching")
conditionalCacheQuery(db)
// Strategy 5: Read-after-write with SelectWrites
fmt.Println("\nStrategy 5: Read-after-write consistency")
readAfterWriteExample(db)
}
// conditionalCacheQuery demonstrates dynamic TTL selection
func conditionalCacheQuery(db *mysql.Database) {
// First query to check result size
var users []User
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `status` = @@status",
0, // No cache for initial check
"active")
if err != nil {
log.Printf("Initial query failed: %v", err)
return
}
// Choose TTL based on result size
var ttl time.Duration
if len(users) > 1000 {
ttl = 30 * time.Minute // Large result - cache longer
fmt.Println(" Large result set (>1000) - using 30min TTL")
} else if len(users) > 100 {
ttl = 10 * time.Minute // Medium result - moderate TTL
fmt.Println(" Medium result set (100-1000) - using 10min TTL")
} else {
ttl = 2 * time.Minute // Small result - short TTL
fmt.Println(" Small result set (<100) - using 2min TTL")
}
// Re-query with appropriate TTL
err = db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `status` = @@status",
ttl,
"active")
if err != nil {
log.Printf("Cached query failed: %v", err)
} else {
fmt.Printf(" ✓ %d users cached with TTL=%v\n", len(users), ttl)
}
}
// readAfterWriteExample demonstrates read-after-write pattern
func readAfterWriteExample(db *mysql.Database) {
// Insert new user
newUser := User{
Name: "CacheUser",
Email: "cache@example.com",
Age: 28,
Active: true,
}
err := db.Insert("users", newUser)
if err != nil {
log.Printf("Insert failed: %v", err)
return
}
fmt.Println(" User inserted")
// WRONG: Using Select() might read from stale cache or replica
// var user User
// db.Select(&user, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
// 5*time.Minute, mysql.Params{"email": "cache@example.com"})
// CORRECT: Use SelectWrites for read-after-write consistency
var user User
err = db.SelectWrites(&user,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
0, // Don't cache write-pool reads
"cache@example.com")
if err != nil {
log.Printf("SelectWrites failed: %v", err)
return
}
fmt.Printf(" ✓ User retrieved immediately (ID: %d)\n", user.ID)
fmt.Println(" ✓ Used write pool for consistency")
}
// performanceBenchmark compares cache vs no-cache performance
func performanceBenchmark(db *mysql.Database) {
fmt.Println("\nPerformance Benchmark: Cache vs No-Cache")
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = @@active"
param := true
// Benchmark without cache
start := time.Now()
iterations := 100
for i := 0; i < iterations; i++ {
var users []User
db.Select(&users, query, 0, param) // No cache
}
noCacheDuration := time.Since(start)
avgNoCache := noCacheDuration / time.Duration(iterations)
fmt.Printf(" Without cache: %v total, %v avg per query\n",
noCacheDuration, avgNoCache)
// Enable cache
db.UseCache(mysql.NewWeakCache())
// Warm up cache
var warmup []User
db.Select(&warmup, query, 5*time.Minute, param)
// Benchmark with cache
start = time.Now()
for i := 0; i < iterations; i++ {
var users []User
db.Select(&users, query, 5*time.Minute, param) // With cache
}
cacheDuration := time.Since(start)
avgCache := cacheDuration / time.Duration(iterations)
fmt.Printf(" With cache: %v total, %v avg per query\n",
cacheDuration, avgCache)
fmt.Printf(" Speedup: %.2fx faster with cache\n",
float64(noCacheDuration)/float64(cacheDuration))
}
// cacheKeyDebug demonstrates understanding cache keys
func cacheKeyDebug(db *mysql.Database) {
fmt.Println("\nCache Key Understanding")
db.UseCache(mysql.NewWeakCache())
// Same query, same params = same cache key
fmt.Println(" 1. Identical queries share cache:")
var users1, users2 []User
db.Select(&users1, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge", 5*time.Minute,
18)
// This hits cache
db.Select(&users2, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge", 5*time.Minute,
18)
fmt.Println(" ✓ Second query used cached result")
// Different params = different cache key
fmt.Println("\n 2. Different params = different cache:")
var users3 []User
db.Select(&users3, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge", 5*time.Minute,
25) // Different param value
fmt.Println(" ✓ Different parameters bypass cache")
// Parameter order doesn't matter
fmt.Println("\n 3. Parameter order normalized:")
var users4, users5 []User
db.Select(&users4,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `active` = @@active",
5*time.Minute,
mysql.Params{"minAge": 18, "active": true})
// Same cache even though params in different order
db.Select(&users5,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `active` = @@active",
5*time.Minute,
mysql.Params{"active": true, "minAge": 18}) // Reversed
fmt.Println(" ✓ Parameter order doesn't affect cache key")
}

View File

@@ -0,0 +1,611 @@
// Package examples demonstrates transaction patterns with cool-mysql
package examples
import (
"context"
"database/sql"
"fmt"
"log"
"strings"
mysql "github.com/StirlingMarketingGroup/cool-mysql"
)
// TransactionExamples demonstrates transaction handling patterns
func TransactionExamples() {
fmt.Println("=== TRANSACTION EXAMPLES ===")
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
// Basic transaction
fmt.Println("\n1. Basic Transaction")
basicTransactionExample()
// Nested transaction handling
fmt.Println("\n2. Nested Transaction (Context-Based)")
nestedTransactionExample()
// Rollback on error
fmt.Println("\n3. Automatic Rollback on Error")
rollbackExample()
// Complex transaction
fmt.Println("\n4. Complex Multi-Step Transaction")
complexTransactionExample()
// Batch transaction
fmt.Println("\n5. Batch Transaction")
batchTransactionExample(context.Background(), db)
// Transaction with retry
fmt.Println("\n6. Transaction with Retry Logic")
transactionWithRetry(context.Background(), db)
// Savepoint example
fmt.Println("\n7. Savepoint Pattern")
savepointExample(context.Background(), db)
// Isolation level
fmt.Println("\n8. Custom Isolation Level")
isolationLevelExample(context.Background(), db)
}
// basicTransactionExample demonstrates basic transaction usage
func basicTransactionExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
ctx := context.Background()
// Get or create transaction
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel() // Always safe to call - rolls back if commit() not called
if err != nil {
log.Printf("Failed to create transaction: %v", err)
return
}
// Store transaction in context
ctx = mysql.NewContextWithTx(ctx, tx)
// Execute operations in transaction
user := User{
Name: "TxUser1",
Email: "tx1@example.com",
Age: 30,
Active: true,
}
err = db.Insert("users", user)
if err != nil {
log.Printf("Insert failed: %v", err)
return // cancel() will rollback
}
fmt.Println(" User inserted in transaction")
// Update in same transaction
err = db.Exec("UPDATE `users` SET `age` = @@age WHERE `email` = @@email",
mysql.Params{
"age": 31,
"email": "tx1@example.com",
})
if err != nil {
log.Printf("Update failed: %v", err)
return // cancel() will rollback
}
fmt.Println(" User updated in transaction")
// Commit transaction
if err := commit(); err != nil {
log.Printf("Commit failed: %v", err)
return
}
fmt.Println("✓ Transaction committed successfully")
}
// nestedTransactionExample demonstrates nested transaction handling
func nestedTransactionExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
ctx := context.Background()
// Start outer transaction
err = outerTransaction(ctx, db)
if err != nil {
log.Printf("Outer transaction failed: %v", err)
return
}
fmt.Println("✓ Nested transactions completed")
}
func outerTransaction(ctx context.Context, db *mysql.Database) error {
// Get or create transaction
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel()
if err != nil {
return fmt.Errorf("outer tx failed: %w", err)
}
// Store in context
ctx = mysql.NewContextWithTx(ctx, tx)
fmt.Println(" Started outer transaction")
// Insert user
user := User{
Name: "OuterTxUser",
Email: "outer@example.com",
Age: 25,
Active: true,
}
err = db.Insert("users", user)
if err != nil {
return fmt.Errorf("outer insert failed: %w", err)
}
fmt.Println(" Outer: User inserted")
// Call inner function with same context
// GetOrCreateTxFromContext will return existing transaction
err = innerTransaction(ctx, db)
if err != nil {
return fmt.Errorf("inner tx failed: %w", err)
}
// Commit outer transaction
if err := commit(); err != nil {
return fmt.Errorf("outer commit failed: %w", err)
}
fmt.Println(" Outer transaction committed")
return nil
}
func innerTransaction(ctx context.Context, db *mysql.Database) error {
// Get or create transaction (will reuse existing from context)
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel()
if err != nil {
return fmt.Errorf("inner tx failed: %w", err)
}
// Transaction already in context, so this is a no-op
ctx = mysql.NewContextWithTx(ctx, tx)
fmt.Println(" Inner: Reusing outer transaction")
// Update user
err = db.Exec("UPDATE `users` SET `age` = @@age WHERE `email` = @@email",
mysql.Params{
"age": 26,
"email": "outer@example.com",
})
if err != nil {
return fmt.Errorf("inner update failed: %w", err)
}
fmt.Println(" Inner: User updated")
// Commit (safe to call, won't actually commit until outer commits)
if err := commit(); err != nil {
return fmt.Errorf("inner commit failed: %w", err)
}
fmt.Println(" Inner: Operations complete")
return nil
}
// rollbackExample demonstrates automatic rollback on error
func rollbackExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
ctx := context.Background()
err = failingTransaction(ctx, db)
if err != nil {
fmt.Printf(" Transaction failed as expected: %v\n", err)
fmt.Println("✓ Transaction automatically rolled back")
}
// Verify rollback - user should not exist
var user User
err = db.Select(&user,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
0,
"rollback@example.com")
if err == sql.ErrNoRows {
fmt.Println("✓ Verified: User was not inserted (rollback worked)")
} else if err != nil {
log.Printf("Verification query failed: %v", err)
} else {
log.Println("✗ Error: User exists (rollback failed)")
}
}
func failingTransaction(ctx context.Context, db *mysql.Database) error {
tx, _, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel() // Will rollback since we don't call commit
if err != nil {
return err
}
ctx = mysql.NewContextWithTx(ctx, tx)
// Insert user
user := User{
Name: "RollbackUser",
Email: "rollback@example.com",
Age: 28,
Active: true,
}
err = db.Insert("users", user)
if err != nil {
return err
}
fmt.Println(" User inserted")
// Simulate error before commit
fmt.Println(" Simulating error...")
return fmt.Errorf("simulated error - transaction will rollback")
// commit() is never called, so cancel() will rollback
}
// complexTransactionExample demonstrates a complex multi-step transaction
func complexTransactionExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
ctx := context.Background()
err = transferFunds(ctx, db, "user1@example.com", "user2@example.com", 100)
if err != nil {
log.Printf("Transfer failed: %v", err)
return
}
fmt.Println("✓ Complex transaction completed")
}
// transferFunds demonstrates a bank transfer-like transaction
func transferFunds(ctx context.Context, db *mysql.Database, fromEmail, toEmail string, amount int) error {
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel()
if err != nil {
return fmt.Errorf("transaction start failed: %w", err)
}
ctx = mysql.NewContextWithTx(ctx, tx)
fmt.Printf(" Starting transfer: %d from %s to %s\n", amount, fromEmail, toEmail)
// Step 1: Check sender balance
type Account struct {
Email string `mysql:"email"`
Balance int `mysql:"balance"`
}
var sender Account
err = db.SelectWrites(&sender,
"SELECT email, balance FROM `accounts` WHERE `email` = @@email FOR UPDATE",
0, // Use write pool for transaction
fromEmail)
if err == sql.ErrNoRows {
return fmt.Errorf("sender account not found")
} else if err != nil {
return fmt.Errorf("failed to fetch sender: %w", err)
}
fmt.Printf(" Sender balance: %d\n", sender.Balance)
// Step 2: Verify sufficient funds
if sender.Balance < amount {
return fmt.Errorf("insufficient funds: have %d, need %d", sender.Balance, amount)
}
// Step 3: Check receiver exists
var receiver Account
err = db.SelectWrites(&receiver,
"SELECT email, balance FROM `accounts` WHERE `email` = @@email FOR UPDATE",
0,
toEmail)
if err == sql.ErrNoRows {
return fmt.Errorf("receiver account not found")
} else if err != nil {
return fmt.Errorf("failed to fetch receiver: %w", err)
}
fmt.Printf(" Receiver balance: %d\n", receiver.Balance)
// Step 4: Deduct from sender
result, err := db.ExecResult(
"UPDATE accounts SET `balance` = balance - @@amount WHERE `email` = @@email",
mysql.Params{
"amount": amount,
"email": fromEmail,
})
if err != nil {
return fmt.Errorf("failed to deduct from sender: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("sender update affected 0 rows")
}
fmt.Printf(" Deducted %d from sender\n", amount)
// Step 5: Add to receiver
result, err = db.ExecResult(
"UPDATE accounts SET `balance` = balance + @@amount WHERE `email` = @@email",
mysql.Params{
"amount": amount,
"email": toEmail,
})
if err != nil {
return fmt.Errorf("failed to add to receiver: %w", err)
}
rows, _ = result.RowsAffected()
if rows == 0 {
return fmt.Errorf("receiver update affected 0 rows")
}
fmt.Printf(" Added %d to receiver\n", amount)
// Step 6: Record transaction log
type TransactionLog struct {
FromEmail string `mysql:"from_email"`
ToEmail string `mysql:"to_email"`
Amount int `mysql:"amount"`
}
txLog := TransactionLog{
FromEmail: fromEmail,
ToEmail: toEmail,
Amount: amount,
}
err = db.Insert("transaction_logs", txLog)
if err != nil {
return fmt.Errorf("failed to log transaction: %w", err)
}
fmt.Println(" Transaction logged")
// Step 7: Commit all changes atomically
if err := commit(); err != nil {
return fmt.Errorf("commit failed: %w", err)
}
fmt.Println(" All changes committed atomically")
return nil
}
// transactionWithRetry demonstrates transaction retry pattern
func transactionWithRetry(ctx context.Context, db *mysql.Database) error {
maxRetries := 3
var err error
for attempt := 1; attempt <= maxRetries; attempt++ {
err = attemptTransaction(ctx, db)
if err == nil {
return nil // Success
}
// Check if error is retryable (e.g., deadlock)
if !isRetryableError(err) {
return err
}
fmt.Printf(" Attempt %d failed (retryable): %v\n", attempt, err)
if attempt < maxRetries {
fmt.Printf(" Retrying... (%d/%d)\n", attempt+1, maxRetries)
}
}
return fmt.Errorf("transaction failed after %d attempts: %w", maxRetries, err)
}
func attemptTransaction(ctx context.Context, db *mysql.Database) error {
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel()
if err != nil {
return err
}
ctx = mysql.NewContextWithTx(ctx, tx)
// Perform transaction operations...
err = db.Insert("users", User{
Name: "RetryUser",
Email: "retry@example.com",
Age: 29,
Active: true,
})
if err != nil {
return err
}
return commit()
}
func isRetryableError(err error) bool {
// Check for MySQL deadlock or lock timeout errors
// Note: cool-mysql already handles automatic retries for these
// This is just an example of manual retry logic
if err == nil {
return false
}
errStr := err.Error()
return strings.Contains(errStr, "deadlock") ||
strings.Contains(errStr, "lock wait timeout") ||
strings.Contains(errStr, "try restarting transaction")
}
// Batch transaction example
func batchTransactionExample(ctx context.Context, db *mysql.Database) error {
fmt.Println("\nBatch Transaction Example")
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel()
if err != nil {
return err
}
ctx = mysql.NewContextWithTx(ctx, tx)
// Insert multiple users in single transaction
users := []User{
{Name: "BatchUser1", Email: "batch1@example.com", Age: 20, Active: true},
{Name: "BatchUser2", Email: "batch2@example.com", Age: 21, Active: true},
{Name: "BatchUser3", Email: "batch3@example.com", Age: 22, Active: true},
}
err = db.Insert("users", users)
if err != nil {
return fmt.Errorf("batch insert failed: %w", err)
}
fmt.Printf(" Inserted %d users in transaction\n", len(users))
// Commit
if err := commit(); err != nil {
return fmt.Errorf("batch commit failed: %w", err)
}
fmt.Println("✓ Batch transaction committed")
return nil
}
// savepoint example (MySQL specific)
func savepointExample(ctx context.Context, db *mysql.Database) error {
fmt.Println("\nSavepoint Example (Advanced)")
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel()
if err != nil {
return err
}
ctx = mysql.NewContextWithTx(ctx, tx)
// Insert first user
user1 := User{Name: "SavepointUser1", Email: "sp1@example.com", Age: 25, Active: true}
err = db.Insert("users", user1)
if err != nil {
return err
}
fmt.Println(" User 1 inserted")
// Create savepoint
err = db.Exec("SAVEPOINT sp1")
if err != nil {
return fmt.Errorf("savepoint creation failed: %w", err)
}
fmt.Println(" Savepoint 'sp1' created")
// Insert second user
user2 := User{Name: "SavepointUser2", Email: "sp2@example.com", Age: 26, Active: true}
err = db.Insert("users", user2)
if err != nil {
return err
}
fmt.Println(" User 2 inserted")
// Simulate error and rollback to savepoint
fmt.Println(" Simulating error, rolling back to savepoint...")
err = db.Exec("ROLLBACK TO SAVEPOINT sp1")
if err != nil {
return fmt.Errorf("rollback to savepoint failed: %w", err)
}
fmt.Println(" Rolled back to savepoint (User 2 not saved)")
// Insert different user
user3 := User{Name: "SavepointUser3", Email: "sp3@example.com", Age: 27, Active: true}
err = db.Insert("users", user3)
if err != nil {
return err
}
fmt.Println(" User 3 inserted")
// Commit transaction
if err := commit(); err != nil {
return fmt.Errorf("commit failed: %w", err)
}
fmt.Println("✓ Transaction committed (User 1 and 3 saved, User 2 rolled back)")
return nil
}
// readCommittedIsolation example
func isolationLevelExample(ctx context.Context, db *mysql.Database) error {
fmt.Println("\nIsolation Level Example")
// Set isolation level before transaction
err := db.Exec("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
if err != nil {
return fmt.Errorf("set isolation level failed: %w", err)
}
fmt.Println(" Isolation level set to READ COMMITTED")
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel()
if err != nil {
return err
}
ctx = mysql.NewContextWithTx(ctx, tx)
// Transaction operations...
var users []User
err = db.SelectWrites(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = 1", 0)
if err != nil {
return err
}
fmt.Printf(" Read %d users with READ COMMITTED isolation\n", len(users))
if err := commit(); err != nil {
return fmt.Errorf("commit failed: %w", err)
}
return nil
}

707
examples/upsert-examples.go Normal file
View File

@@ -0,0 +1,707 @@
// Package examples demonstrates upsert patterns with cool-mysql
package examples
import (
"fmt"
"log"
"time"
mysql "github.com/StirlingMarketingGroup/cool-mysql"
)
// UpsertExamples demonstrates INSERT ... ON DUPLICATE KEY UPDATE patterns
func UpsertExamples() {
fmt.Println("=== UPSERT EXAMPLES ===")
// Basic upsert
fmt.Println("\n1. Basic Upsert (by unique key)")
basicUpsertExample()
// Upsert with multiple unique columns
fmt.Println("\n2. Upsert with Composite Unique Key")
compositeKeyUpsertExample()
// Conditional upsert
fmt.Println("\n3. Conditional Upsert with WHERE clause")
conditionalUpsertExample()
// Batch upsert
fmt.Println("\n4. Batch Upsert")
batchUpsertExample()
// Selective column updates
fmt.Println("\n5. Selective Column Updates")
selectiveUpdateExample()
// Timestamp tracking
fmt.Println("\n6. Timestamp Tracking with Upsert")
timestampUpsertExample()
// Increment counter
fmt.Println("\n7. Increment Counter Pattern")
incrementCounterExample()
// Upsert from channel
fmt.Println("\n8. Upsert from Channel (streaming)")
upsertFromChannelExample()
// Upsert vs Insert Ignore
fmt.Println("\n9. Upsert vs Insert Ignore")
UpsertOrIgnoreExample()
}
// basicUpsertExample demonstrates simple upsert by email
func basicUpsertExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
// Define user with unique email
user := User{
Name: "UpsertUser",
Email: "upsert@example.com", // Unique key
Age: 30,
Active: true,
}
// First upsert - INSERT (user doesn't exist)
err = db.Upsert(
"users", // table
[]string{"email"}, // unique columns (conflict detection)
[]string{"name", "age", "active"}, // columns to update on conflict
"", // no WHERE clause
user, // data
)
if err != nil {
log.Printf("First upsert failed: %v", err)
return
}
fmt.Println(" First upsert: User inserted")
// Second upsert - UPDATE (user exists)
user.Name = "UpsertUser Updated"
user.Age = 31
err = db.Upsert(
"users",
[]string{"email"},
[]string{"name", "age", "active"},
"",
user,
)
if err != nil {
log.Printf("Second upsert failed: %v", err)
return
}
fmt.Println(" Second upsert: User updated")
// Verify result
var retrieved User
err = db.Select(&retrieved,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
0,
"upsert@example.com")
if err != nil {
log.Printf("Verification failed: %v", err)
return
}
fmt.Printf("✓ Final state: Name='%s', Age=%d\n", retrieved.Name, retrieved.Age)
}
// compositeKeyUpsertExample demonstrates upsert with multiple unique columns
func compositeKeyUpsertExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
type ProductInventory struct {
StoreID int `mysql:"store_id"`
ProductID int `mysql:"product_id"`
Quantity int `mysql:"quantity"`
Price float64 `mysql:"price"`
}
// Initial inventory
inventory := ProductInventory{
StoreID: 1,
ProductID: 100,
Quantity: 50,
Price: 19.99,
}
// First upsert - INSERT
err = db.Upsert(
"inventory",
[]string{"store_id", "product_id"}, // Composite unique key
[]string{"quantity", "price"}, // Update these on conflict
"",
inventory,
)
if err != nil {
log.Printf("First upsert failed: %v", err)
return
}
fmt.Println(" Inventory created: Store 1, Product 100, Qty=50, Price=$19.99")
// Second upsert - UPDATE existing inventory
inventory.Quantity = 75
inventory.Price = 17.99
err = db.Upsert(
"inventory",
[]string{"store_id", "product_id"},
[]string{"quantity", "price"},
"",
inventory,
)
if err != nil {
log.Printf("Second upsert failed: %v", err)
return
}
fmt.Println(" Inventory updated: Qty=75, Price=$17.99")
fmt.Println("✓ Composite key upsert successful")
}
// conditionalUpsertExample demonstrates upsert with WHERE clause
func conditionalUpsertExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
type Document struct {
ID int `mysql:"id"`
Title string `mysql:"title"`
Content string `mysql:"content"`
Version int `mysql:"version"`
UpdatedAt time.Time `mysql:"updated_at"`
}
// Initial document
doc := Document{
ID: 1,
Title: "My Document",
Content: "Version 1 content",
Version: 1,
UpdatedAt: time.Now(),
}
// Insert initial version
err = db.Insert("documents", doc)
if err != nil {
log.Printf("Insert failed: %v", err)
return
}
fmt.Println(" Document v1 created")
// Upsert with condition: only update if newer
doc.Content = "Version 2 content"
doc.Version = 2
doc.UpdatedAt = time.Now()
err = db.Upsert(
"documents",
[]string{"id"},
[]string{"title", "content", "version", "updated_at"},
"version < VALUES(version)", // Only update if new version is higher
doc,
)
if err != nil {
log.Printf("Conditional upsert failed: %v", err)
return
}
fmt.Println(" Document updated to v2 (condition met)")
// Try to upsert with older version (should not update)
oldDoc := Document{
ID: 1,
Title: "My Document",
Content: "Old content",
Version: 1, // Older version
UpdatedAt: time.Now().Add(-time.Hour),
}
err = db.Upsert(
"documents",
[]string{"id"},
[]string{"title", "content", "version", "updated_at"},
"version < VALUES(version)",
oldDoc,
)
if err != nil {
log.Printf("Old version upsert failed: %v", err)
return
}
fmt.Println(" Old version upsert executed (but condition prevented update)")
// Verify current version
var current Document
err = db.Select(&current,
"SELECT `id`, `title`, `content`, `version`, `updated_at` FROM `documents` WHERE `id` = @@id",
0,
1)
if err != nil {
log.Printf("Verification failed: %v", err)
return
}
fmt.Printf("✓ Final version: %d (conditional update worked)\n", current.Version)
}
// batchUpsertExample demonstrates upserting multiple records
func batchUpsertExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
type Setting struct {
Key string `mysql:"key"`
Value string `mysql:"value"`
}
// Batch of settings
settings := []Setting{
{Key: "theme", Value: "dark"},
{Key: "language", Value: "en"},
{Key: "notifications", Value: "enabled"},
{Key: "timezone", Value: "UTC"},
}
// Upsert all settings
err = db.Upsert(
"settings",
[]string{"key"}, // Unique on key
[]string{"value"}, // Update value on conflict
"",
settings,
)
if err != nil {
log.Printf("Batch upsert failed: %v", err)
return
}
fmt.Printf(" Inserted/updated %d settings\n", len(settings))
// Update some settings
updatedSettings := []Setting{
{Key: "theme", Value: "light"}, // Changed
{Key: "language", Value: "es"}, // Changed
{Key: "notifications", Value: "enabled"}, // Same
{Key: "font_size", Value: "14"}, // New
}
err = db.Upsert(
"settings",
[]string{"key"},
[]string{"value"},
"",
updatedSettings,
)
if err != nil {
log.Printf("Update batch upsert failed: %v", err)
return
}
fmt.Printf(" Updated batch: 2 changed, 1 same, 1 new\n")
// Verify results
var allSettings []Setting
err = db.Select(&allSettings,
"SELECT `key`, `value` FROM `settings` ORDER BY key",
0)
if err != nil {
log.Printf("Verification failed: %v", err)
return
}
fmt.Printf("✓ Total settings: %d\n", len(allSettings))
for _, s := range allSettings {
fmt.Printf(" %s = %s\n", s.Key, s.Value)
}
}
// selectiveUpdateExample demonstrates updating only specific columns
func selectiveUpdateExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
type UserProfile struct {
Email string `mysql:"email"`
Name string `mysql:"name"`
Bio string `mysql:"bio"`
Avatar string `mysql:"avatar"`
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
}
// Initial profile
profile := UserProfile{
Email: "profile@example.com",
Name: "John Doe",
Bio: "Software Developer",
Avatar: "avatar1.jpg",
UpdatedAt: time.Now(),
}
err = db.Insert("user_profiles", profile)
if err != nil {
log.Printf("Insert failed: %v", err)
return
}
fmt.Println(" Profile created")
// Update only name and bio (not avatar)
profile.Name = "John Smith"
profile.Bio = "Senior Software Developer"
// Avatar unchanged
err = db.Upsert(
"user_profiles",
[]string{"email"},
[]string{"name", "bio", "updated_at"}, // Don't include avatar
"",
profile,
)
if err != nil {
log.Printf("Selective update failed: %v", err)
return
}
fmt.Println(" Updated name and bio (avatar unchanged)")
// Later, update only avatar
profile.Avatar = "avatar2.jpg"
err = db.Upsert(
"user_profiles",
[]string{"email"},
[]string{"avatar", "updated_at"}, // Only avatar
"",
profile,
)
if err != nil {
log.Printf("Avatar update failed: %v", err)
return
}
fmt.Println(" Updated avatar only")
// Verify
var final UserProfile
err = db.Select(&final,
"SELECT `email`, `name`, `bio`, `avatar`, `updated_at` FROM `user_profiles` WHERE `email` = @@email",
0,
"profile@example.com")
if err != nil {
log.Printf("Verification failed: %v", err)
return
}
fmt.Printf("✓ Final: Name='%s', Bio='%s', Avatar='%s'\n",
final.Name, final.Bio, final.Avatar)
}
// timestampUpsertExample demonstrates tracking created_at and updated_at
func timestampUpsertExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
type Article struct {
Slug string `mysql:"slug"`
Title string `mysql:"title"`
Content string `mysql:"content"`
Views int `mysql:"views"`
CreatedAt time.Time `mysql:"created_at,defaultzero"`
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
}
// Initial article
article := Article{
Slug: "my-article",
Title: "My Article",
Content: "Initial content",
Views: 0,
}
// First upsert - INSERT
// CreatedAt and UpdatedAt will use database defaults
err = db.Upsert(
"articles",
[]string{"slug"},
[]string{"title", "content", "views", "updated_at"},
"",
article,
)
if err != nil {
log.Printf("Insert failed: %v", err)
return
}
fmt.Println(" Article created (created_at set by DB)")
// Get the article to see timestamps
var inserted Article
err = db.Select(&inserted,
"SELECT `slug`, `title`, `content`, `views`, `created_at`, `updated_at` FROM `articles` WHERE slug = @@slug",
0,
"my-article")
if err != nil {
log.Printf("Select failed: %v", err)
return
}
fmt.Printf(" Created at: %s\n", inserted.CreatedAt.Format(time.RFC3339))
time.Sleep(2 * time.Second) // Wait to see time difference
// Update article
article.Title = "My Updated Article"
article.Content = "Updated content"
article.Views = inserted.Views + 10
err = db.Upsert(
"articles",
[]string{"slug"},
[]string{"title", "content", "views", "updated_at"},
"", // Don't update created_at
article,
)
if err != nil {
log.Printf("Update failed: %v", err)
return
}
fmt.Println(" Article updated (updated_at changed, created_at preserved)")
// Verify timestamps
var updated Article
err = db.Select(&updated,
"SELECT `slug`, `title`, `content`, `views`, `created_at`, `updated_at` FROM `articles` WHERE slug = @@slug",
0,
"my-article")
if err != nil {
log.Printf("Verification failed: %v", err)
return
}
fmt.Printf(" Created at: %s (unchanged)\n", updated.CreatedAt.Format(time.RFC3339))
fmt.Printf(" Updated at: %s (newer)\n", updated.UpdatedAt.Format(time.RFC3339))
fmt.Printf("✓ Timestamps tracked correctly\n")
}
// incrementCounterExample demonstrates atomic counter updates
func incrementCounterExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
fmt.Println("\nIncrement Counter Example")
type PageView struct {
Page string `mysql:"page"`
Views int `mysql:"views"`
}
page := "homepage"
// Increment views (insert with 1 if not exists, increment if exists)
// This uses MySQL's VALUES() function to reference the insert value
viewRecord := PageView{
Page: page,
Views: 1, // Initial value for insert
}
// Custom upsert to increment on duplicate
err = db.Exec(
"INSERT INTO `page_views` (`page`, `views`)"+
" VALUES (@@page, @@views)"+
" ON DUPLICATE KEY UPDATE `views` = `views` + @@views",
mysql.Params{
"page": viewRecord.Page,
"views": viewRecord.Views,
})
if err != nil {
log.Printf("Increment failed: %v", err)
return
}
fmt.Printf(" View count incremented for %s\n", page)
// Increment again
err = db.Exec(
"INSERT INTO `page_views` (`page`, `views`)"+
" VALUES (@@page, @@views)"+
" ON DUPLICATE KEY UPDATE `views` = `views` + @@views",
mysql.Params{
"page": page,
"views": 1,
})
if err != nil {
log.Printf("Second increment failed: %v", err)
return
}
// Check total views
var totalViews int
err = db.Select(&totalViews,
"SELECT views FROM `page_views` WHERE page = @@page",
0,
page)
if err != nil {
log.Printf("Select views failed: %v", err)
return
}
fmt.Printf("✓ Total views for %s: %d\n", page, totalViews)
}
// upsertFromChannelExample demonstrates streaming upserts
func upsertFromChannelExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
fmt.Println("\nUpsert from Channel Example")
type Metric struct {
MetricName string `mysql:"metric_name"`
Value float64 `mysql:"value"`
Timestamp time.Time `mysql:"timestamp,defaultzero"`
}
// Create channel of metrics
metricCh := make(chan Metric, 100)
// Producer: generate metrics
go func() {
defer close(metricCh)
metrics := []string{"cpu", "memory", "disk", "network"}
for i := 0; i < 100; i++ {
metricCh <- Metric{
MetricName: metrics[i%len(metrics)],
Value: float64(i),
Timestamp: time.Now(),
}
}
}()
// Upsert from channel
err = db.Upsert(
"metrics",
[]string{"metric_name"},
[]string{"value", "timestamp"},
"",
metricCh,
)
if err != nil {
log.Printf("Channel upsert failed: %v", err)
return
}
fmt.Println("✓ Streamed 100 metric upserts")
// Verify
var count int
count, err = db.Count("SELECT COUNT(*) FROM `metrics`", 0)
if err != nil {
log.Printf("Count failed: %v", err)
return
}
fmt.Printf(" Total unique metrics: %d\n", count)
}
// UpsertOrIgnoreExample demonstrates choosing between update and ignore
func UpsertOrIgnoreExample() {
db, err := setupDatabase()
if err != nil {
log.Printf("Setup failed: %v", err)
return
}
fmt.Println("\nUpsert vs Insert Ignore Example")
type UniqueEmail struct {
Email string `mysql:"email"`
Count int `mysql:"count"`
}
// With UPSERT - updates on duplicate
email1 := UniqueEmail{Email: "test1@example.com", Count: 1}
err = db.Upsert(
"email_counts",
[]string{"email"},
[]string{"count"},
"",
email1,
)
fmt.Println(" Upsert: Inserts or updates")
// With INSERT IGNORE - silently ignores duplicate
err = db.Exec(
"INSERT IGNORE INTO `email_counts` (email, count) VALUES (@@email, @@count)",
mysql.Params{"email": "test1@example.com", "count": 999})
fmt.Println(" Insert Ignore: Keeps original value on duplicate")
// Verify - count should still be 1 (ignore worked)
var result UniqueEmail
err = db.Select(&result,
"SELECT `email`, `count` FROM `email_counts` WHERE `email` = @@email",
0,
"test1@example.com")
if err != nil {
log.Printf("Verification failed: %v", err)
return
}
fmt.Printf("✓ Count=%d (INSERT IGNORE kept original)\n", result.Count)
}

85
plugin.lock.json Normal file
View File

@@ -0,0 +1,85 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:StirlingMarketingGroup/cool-mysql:.claude/skills/cool-mysql",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "34c0180280fa340253f8abb62e925db1a962b99d",
"treeHash": "25649f306022076993da9a68020db65922c8f25a642b92b9cd11a9f90027dbf0",
"generatedAt": "2025-11-28T10:12:49.515283Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "cool-mysql",
"description": "Comprehensive guide for cool-mysql Go library - MySQL helper with dual connection pools, named parameters, template syntax, caching, and advanced query patterns",
"version": null
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "818751d7ad2db0c435738b0e7c464bdc1b3b5ef125c39b90187b0fd92c4683db"
},
{
"path": "SKILL.md",
"sha256": "71c7038291daa7b6e032373665a79adaa6ccf3235708e3ff3394ba03252dbc61"
},
{
"path": "references/query-patterns.md",
"sha256": "dc4953d6868e86b5a0ee3264f4a20b3a18ec7915167afdf27aa5b81f9f82c221"
},
{
"path": "references/struct-tags.md",
"sha256": "82e64aedfd0048db171c7ce0539c0d4710728e8c946de022f735f7244f2c66f5"
},
{
"path": "references/caching-guide.md",
"sha256": "a422f96300ab93af34cb9bfcb4a043176342e9609a873b76830642c07f2c0cf3"
},
{
"path": "references/testing-patterns.md",
"sha256": "3ea21a3482f7ffbac5ae0c274b108732f1b612dbdaa26794aed7a931d85ae5fd"
},
{
"path": "references/api-reference.md",
"sha256": "81acaace8fb78dadbab09c5499956882c2dce2fbe77e71b27dfdddc4de015f8a"
},
{
"path": "examples/basic-crud.go",
"sha256": "ba3022b5567fab5bc10853e45d02be862d3413e80bfb4bf841a259392f5ba222"
},
{
"path": "examples/upsert-examples.go",
"sha256": "f5cdf080cece284d5eb31a24028355c81dd0733608bb7720eff576be2b6cbe99"
},
{
"path": "examples/advanced-queries.go",
"sha256": "ad278e8539ee60fafaec3a86c84c339b0f6bb78552e82cc18846e92e730928ce"
},
{
"path": "examples/caching-setup.go",
"sha256": "3a7933279518646d31a2ed51d9961883788d4345c2e7f28cfb961d794bf25cf6"
},
{
"path": "examples/transaction-patterns.go",
"sha256": "7161ffe6726b7cea5f7314affdeeb7c65a98dfdb69f69c048fbb1ccab6e28e9d"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "661e81c25dd221cd4ae38d538bbeae6dd65fec9226370dfba036d1467ac8a05e"
}
],
"dirSha256": "25649f306022076993da9a68020db65922c8f25a642b92b9cd11a9f90027dbf0"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

792
references/api-reference.md Normal file
View File

@@ -0,0 +1,792 @@
# cool-mysql API Reference
Complete API documentation for all cool-mysql methods, organized by category.
## Database Creation
### New
```go
func New(wUser, wPass, wSchema, wHost string, wPort int,
rUser, rPass, rSchema, rHost string, rPort int,
collation, timeZone string) (*Database, error)
```
Create a new database connection from connection parameters.
**Parameters:**
- `wUser`, `wPass`, `wSchema`, `wHost`, `wPort` - Write connection credentials
- `rUser`, `rPass`, `rSchema`, `rHost`, `rPort` - Read connection credentials
- `collation` - Database collation (e.g., `"utf8mb4_unicode_ci"`)
- `timeZone` - Time zone for connections (e.g., `"America/New_York"`, `"UTC"`)
**Returns:**
- `*Database` - Database instance with dual connection pools
- `error` - Connection error if unable to establish connections
**Example:**
```go
db, err := mysql.New(
"root", "password", "mydb", "localhost", 3306,
"root", "password", "mydb", "localhost", 3306,
"utf8mb4_unicode_ci",
"UTC",
)
```
### NewFromDSN
```go
func NewFromDSN(writesDSN, readsDSN string) (*Database, error)
```
Create database connection from DSN strings.
**Parameters:**
- `writesDSN` - Write connection DSN
- `readsDSN` - Read connection DSN
**DSN Format:**
```
username:password@protocol(address)/dbname?param=value
```
**Example:**
```go
writesDSN := "user:pass@tcp(write-host:3306)/dbname?parseTime=true&loc=UTC"
readsDSN := "user:pass@tcp(read-host:3306)/dbname?parseTime=true&loc=UTC"
db, err := mysql.NewFromDSN(writesDSN, readsDSN)
```
### NewFromConn
```go
func NewFromConn(writesConn, readsConn *sql.DB) (*Database, error)
```
Create database from existing `*sql.DB` connections.
**Parameters:**
- `writesConn` - Existing write connection
- `readsConn` - Existing read connection
**Example:**
```go
writesConn, _ := sql.Open("mysql", writesDSN)
readsConn, _ := sql.Open("mysql", readsDSN)
db, err := mysql.NewFromConn(writesConn, readsConn)
```
## Query Methods (SELECT)
### Select
```go
func (db *Database) Select(dest any, query string, cacheTTL time.Duration, params ...mysql.Params) error
```
Execute SELECT query and scan results into destination. Uses read connection pool.
**Parameters:**
- `dest` - Destination for results (struct, slice, map, channel, function, or primitive)
- `query` - SQL query with `@@paramName` placeholders
- `cacheTTL` - Cache duration (`0` = no cache, `> 0` = cache for duration)
- `params` - Query parameters (`mysql.Params{}` or structs)
**Destination Types:**
- `*[]StructType` - Slice of structs
- `*StructType` - Single struct (returns `sql.ErrNoRows` if not found)
- `*string`, `*int`, `*time.Time`, etc. - Single value
- `chan StructType` - Channel for streaming results
- `func(StructType)` - Function called for each row
- `*[]map[string]any` - Slice of maps
- `*json.RawMessage` - JSON result
**Returns:**
- `error` - Query error or `sql.ErrNoRows` for single-value queries with no results
**Examples:**
```go
// Select into struct slice
var users []User
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge", 5*time.Minute,
18)
// Select single value
var name string
err := db.Select(&name, "SELECT `name` FROM `users` WHERE `id` = @@id", 0,
1)
// Select into channel
userCh := make(chan User)
go func() {
defer close(userCh)
db.Select(userCh, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
}()
// Select with function
db.Select(func(u User) {
log.Printf("User: %s", u.Name)
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
```
### SelectContext
```go
func (db *Database) SelectContext(ctx context.Context, dest any, query string,
cacheTTL time.Duration, params ...mysql.Params) error
```
Context-aware version of `Select()`. Supports cancellation and deadlines.
**Parameters:**
- `ctx` - Context for cancellation/timeout
- Additional parameters same as `Select()`
**Example:**
```go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var users []User
err := db.SelectContext(ctx, &users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
```
### SelectWrites
```go
func (db *Database) SelectWrites(dest any, query string, cacheTTL time.Duration,
params ...mysql.Params) error
```
Select using write connection pool. Use for read-after-write consistency.
**When to Use:**
- Immediately after INSERT/UPDATE/DELETE when you need to read the modified data
- When you need strong consistency and can't risk reading stale replica data
**Example:**
```go
// Insert then immediately read
db.Insert("users", user)
db.SelectWrites(&user, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id", 0,
user.ID)
```
### SelectWritesContext
```go
func (db *Database) SelectWritesContext(ctx context.Context, dest any, query string,
cacheTTL time.Duration, params ...mysql.Params) error
```
Context-aware version of `SelectWrites()`.
### SelectJSON
```go
func (db *Database) SelectJSON(dest *json.RawMessage, query string,
cacheTTL time.Duration, params ...mysql.Params) error
```
Select query results as JSON.
**Example:**
```go
var result json.RawMessage
err := db.SelectJSON(&result,
"SELECT JSON_OBJECT('id', id, 'name', name) FROM `users` WHERE `id` = @@id",
0, 1)
```
### SelectJSONContext
```go
func (db *Database) SelectJSONContext(ctx context.Context, dest *json.RawMessage,
query string, cacheTTL time.Duration,
params ...mysql.Params) error
```
Context-aware version of `SelectJSON()`.
## Utility Query Methods
### Count
```go
func (db *Database) Count(query string, cacheTTL time.Duration, params ...mysql.Params) (int64, error)
```
Execute COUNT query and return result as `int64`. Uses read pool.
**Parameters:**
- `query` - Query that returns a single integer (typically `SELECT COUNT(*)`)
- `cacheTTL` - Cache duration
- `params` - Query parameters
**Returns:**
- `int64` - Count result
- `error` - Query error
**Example:**
```go
count, err := db.Count("SELECT COUNT(*) FROM `users` WHERE `active` = @@active",
5*time.Minute, 1)
```
### CountContext
```go
func (db *Database) CountContext(ctx context.Context, query string, cacheTTL time.Duration,
params ...mysql.Params) (int64, error)
```
Context-aware version of `Count()`.
### Exists
```go
func (db *Database) Exists(query string, cacheTTL time.Duration, params ...mysql.Params) (bool, error)
```
Check if query returns any rows. Uses read pool.
**Parameters:**
- `query` - Query to check (typically `SELECT 1 FROM ... WHERE ...`)
- `cacheTTL` - Cache duration
- `params` - Query parameters
**Returns:**
- `bool` - `true` if rows exist, `false` otherwise
- `error` - Query error
**Example:**
```go
exists, err := db.Exists("SELECT 1 FROM `users` WHERE `email` = @@email", 0,
"user@example.com")
```
### ExistsContext
```go
func (db *Database) ExistsContext(ctx context.Context, query string, cacheTTL time.Duration,
params ...mysql.Params) (bool, error)
```
Context-aware version of `Exists()`.
### ExistsWrites
```go
func (db *Database) ExistsWrites(query string, params ...mysql.Params) (bool, error)
```
Check existence using write pool for read-after-write consistency.
### ExistsWritesContext
```go
func (db *Database) ExistsWritesContext(ctx context.Context, query string,
params ...mysql.Params) (bool, error)
```
Context-aware version of `ExistsWrites()`.
## Insert Operations
### Insert
```go
func (db *Database) Insert(table string, data any) error
```
Insert data into table. Automatically chunks large batches based on `max_allowed_packet`.
**Parameters:**
- `table` - Table name
- `data` - Single struct, slice of structs, or channel of structs
**Returns:**
- `error` - Insert error
**Examples:**
```go
// Single insert
user := User{Name: "Alice", Email: "alice@example.com"}
err := db.Insert("users", user)
// Batch insert
users := []User{
{Name: "Bob", Email: "bob@example.com"},
{Name: "Charlie", Email: "charlie@example.com"},
}
err := db.Insert("users", users)
// Streaming insert
userCh := make(chan User)
go func() {
for _, u := range users {
userCh <- u
}
close(userCh)
}()
err := db.Insert("users", userCh)
```
### InsertContext
```go
func (db *Database) InsertContext(ctx context.Context, table string, data any) error
```
Context-aware version of `Insert()`.
**Example:**
```go
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := db.InsertContext(ctx, "users", users)
```
## Upsert Operations
### Upsert
```go
func (db *Database) Upsert(table string, uniqueCols, updateCols []string,
where string, data any) error
```
Perform INSERT ... ON DUPLICATE KEY UPDATE operation.
**Parameters:**
- `table` - Table name
- `uniqueCols` - Columns that define uniqueness (used in conflict detection)
- `updateCols` - Columns to update on duplicate key
- `where` - Optional WHERE clause for conditional update (can be empty)
- `data` - Single struct, slice of structs, or channel of structs
**Returns:**
- `error` - Upsert error
**Examples:**
```go
// Basic upsert on unique email
err := db.Upsert(
"users",
[]string{"email"}, // unique column
[]string{"name", "updated_at"}, // columns to update
"", // no WHERE clause
user,
)
// Upsert with conditional update
err := db.Upsert(
"users",
[]string{"id"},
[]string{"name", "email"},
"updated_at < VALUES(updated_at)", // only update if newer
users,
)
// Batch upsert
err := db.Upsert(
"users",
[]string{"email"},
[]string{"name", "last_login"},
"",
[]User{{Email: "a@example.com", Name: "Alice"}, ...},
)
```
### UpsertContext
```go
func (db *Database) UpsertContext(ctx context.Context, table string, uniqueCols,
updateCols []string, where string, data any) error
```
Context-aware version of `Upsert()`.
## Execute Operations
### Exec
```go
func (db *Database) Exec(query string, params ...mysql.Params) error
```
Execute query without returning results (UPDATE, DELETE, etc.). Uses write pool.
**Parameters:**
- `query` - SQL query with `@@paramName` placeholders
- `params` - Query parameters
**Returns:**
- `error` - Execution error
**Example:**
```go
err := db.Exec("UPDATE `users` SET `active` = @@active WHERE `id` = @@id",
mysql.Params{"active": 1, "id": 123})
err := db.Exec("DELETE FROM `users` WHERE last_login < @@cutoff",
time.Now().Add(-365*24*time.Hour))
```
### ExecContext
```go
func (db *Database) ExecContext(ctx context.Context, query string, params ...mysql.Params) error
```
Context-aware version of `Exec()`.
### ExecResult
```go
func (db *Database) ExecResult(query string, params ...mysql.Params) (sql.Result, error)
```
Execute query and return `sql.Result` for accessing `LastInsertId()` and `RowsAffected()`.
**Returns:**
- `sql.Result` - Execution result
- `error` - Execution error
**Example:**
```go
result, err := db.ExecResult("UPDATE `users` SET `name` = @@name WHERE `id` = @@id",
mysql.Params{"name": "Alice", "id": 1})
if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
log.Printf("Updated %d rows", rowsAffected)
```
### ExecResultContext
```go
func (db *Database) ExecResultContext(ctx context.Context, query string,
params ...mysql.Params) (sql.Result, error)
```
Context-aware version of `ExecResult()`.
## Transaction Management
### GetOrCreateTxFromContext
```go
func GetOrCreateTxFromContext(ctx context.Context) (*sql.Tx, func() error, func(), error)
```
Get existing transaction from context or create new one.
**Returns:**
- `*sql.Tx` - Transaction instance
- `func() error` - Commit function
- `func()` - Cancel function (rolls back if not committed)
- `error` - Transaction creation error
**Usage Pattern:**
```go
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel() // Always safe to call - rolls back if commit() not called
if err != nil {
return err
}
// Store transaction in context
ctx = mysql.NewContextWithTx(ctx, tx)
// Do database operations...
if err := commit(); err != nil {
return err
}
```
### NewContextWithTx
```go
func NewContextWithTx(ctx context.Context, tx *sql.Tx) context.Context
```
Store transaction in context for use by database operations.
### TxFromContext
```go
func TxFromContext(ctx context.Context) (*sql.Tx, bool)
```
Retrieve transaction from context.
**Returns:**
- `*sql.Tx` - Transaction if present
- `bool` - `true` if transaction exists in context
## Context Management
### NewContext
```go
func NewContext(ctx context.Context, db *Database) context.Context
```
Store database instance in context.
**Example:**
```go
ctx := mysql.NewContext(context.Background(), db)
```
### NewContextWithFunc
```go
func NewContextWithFunc(ctx context.Context, f func() *Database) context.Context
```
Store database factory function in context for lazy initialization.
**Example:**
```go
ctx := mysql.NewContextWithFunc(ctx, sync.OnceValue(func() *Database {
db, err := mysql.New(...)
if err != nil {
panic(err)
}
return db
}))
```
### FromContext
```go
func FromContext(ctx context.Context) *Database
```
Retrieve database from context.
**Returns:**
- `*Database` - Database instance or `nil` if not found
**Example:**
```go
db := mysql.FromContext(ctx)
if db == nil {
return errors.New("database not in context")
}
```
## Caching Configuration
### EnableRedis
```go
func (db *Database) EnableRedis(client *redis.Client)
```
Enable Redis caching with distributed locking.
**Example:**
```go
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
db.EnableRedis(redisClient)
```
### EnableMemcache
```go
func (db *Database) EnableMemcache(client *memcache.Client)
```
Enable Memcached caching.
**Example:**
```go
memcacheClient := memcache.New("localhost:11211")
db.EnableMemcache(memcacheClient)
```
### UseCache
```go
func (db *Database) UseCache(cache Cache)
```
Use custom cache implementation.
**Examples:**
```go
// In-memory cache
db.UseCache(mysql.NewWeakCache())
// Multi-level cache
db.UseCache(mysql.NewMultiCache(
mysql.NewWeakCache(), // L1: Local fast cache
mysql.NewRedisCache(redisClient), // L2: Distributed cache
))
```
### NewWeakCache
```go
func NewWeakCache() *WeakCache
```
Create in-memory cache with weak pointers (GC-managed).
### NewRedisCache
```go
func NewRedisCache(client *redis.Client) *RedisCache
```
Create Redis cache with distributed locking support.
### NewMultiCache
```go
func NewMultiCache(caches ...Cache) *MultiCache
```
Create layered cache that checks caches in order.
## Parameter Interpolation
### InterpolateParams
```go
func (db *Database) InterpolateParams(query string, params ...mysql.Params) (string, []any, error)
```
Manually interpolate parameters in query. Useful for debugging or logging.
**Parameters:**
- `query` - Query with `@@paramName` placeholders
- `params` - Parameters to interpolate
**Returns:**
- `string` - Query with `?` placeholders
- `[]any` - Normalized parameter values
- `error` - Interpolation error
**Example:**
```go
replacedQuery, normalizedParams, err := db.InterpolateParams(
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id",
mysql.Params{"id": 1},
)
// replacedQuery: "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = ?"
// normalizedParams: []any{1}
```
## Template Functions
### AddTemplateFuncs
```go
func (db *Database) AddTemplateFuncs(funcs template.FuncMap)
```
Add custom functions available in query templates.
**Example:**
```go
db.AddTemplateFuncs(template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
})
db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `name` = @@name{{ if .UpperCase }} COLLATE utf8mb4_bin{{ end }}",
0,
mysql.Params{"name": "alice", "upperCase": true})
```
## Special Types
### Params
```go
type Params map[string]any
```
Parameter map for query placeholders.
### Raw
```go
type Raw string
```
Literal SQL that won't be escaped. **Use with caution - SQL injection risk.**
**Example:**
```go
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE @@condition", 0,
mysql.Params{
"condition": mysql.Raw("created_at > NOW() - INTERVAL 1 DAY"),
})
```
### MapRow / SliceRow / MapRows / SliceRows
```go
type MapRow map[string]any
type SliceRow []any
type MapRows []map[string]any
type SliceRows [][]any
```
Flexible result types when struct mapping isn't needed.
**Example:**
```go
var rows mysql.MapRows
db.Select(&rows, "SELECT `id`, name FROM `users`", 0)
for _, row := range rows {
fmt.Printf("ID: %v, Name: %v\n", row["id"], row["name"])
}
```
## Custom Interfaces
### Zeroer
```go
type Zeroer interface {
IsZero() bool
}
```
Implement for custom zero-value detection with `defaultzero` tag.
**Example:**
```go
type CustomTime struct {
time.Time
}
func (ct CustomTime) IsZero() bool {
return ct.Time.IsZero() || ct.Time.Unix() == 0
}
```
### Valueser
```go
type Valueser interface {
Values() []any
}
```
Implement for custom value conversion during inserts.
**Example:**
```go
type Point struct {
X, Y float64
}
func (p Point) Values() []any {
return []any{p.X, p.Y}
}
```
## Error Handling
### Automatic Retries
cool-mysql automatically retries these MySQL error codes:
- `1213` - Deadlock detected
- `1205` - Lock wait timeout exceeded
- `2006` - MySQL server has gone away
- `2013` - Lost connection to MySQL server during query
Retry behavior uses exponential backoff and can be configured with `COOL_MAX_ATTEMPTS` environment variable.
### sql.ErrNoRows
- **Single value/struct queries**: Returns `sql.ErrNoRows` when no results
- **Slice queries**: Returns empty slice (not `sql.ErrNoRows`)
**Example:**
```go
var name string
err := db.Select(&name, "SELECT `name` FROM `users` WHERE `id` = @@id", 0,
999)
if errors.Is(err, sql.ErrNoRows) {
// Handle not found
}
var users []User
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id", 0,
999)
// err is nil, users is empty slice []
```

700
references/caching-guide.md Normal file
View File

@@ -0,0 +1,700 @@
# Caching Guide
Complete guide to caching strategies and configuration in cool-mysql.
## Table of Contents
1. [Caching Overview](#caching-overview)
2. [Cache Types](#cache-types)
3. [Cache Configuration](#cache-configuration)
4. [TTL Selection](#ttl-selection)
5. [Multi-Level Caching](#multi-level-caching)
6. [Cache Invalidation](#cache-invalidation)
7. [Distributed Locking](#distributed-locking)
8. [Performance Optimization](#performance-optimization)
9. [Best Practices](#best-practices)
## Caching Overview
cool-mysql supports pluggable caching for SELECT queries to reduce database load and improve response times.
### How Caching Works
1. **Cache Key Generation**: Automatically generated from query + parameters
2. **Cache Check**: Before executing query, check cache for existing result
3. **Cache Miss**: Execute query and store result with TTL
4. **Cache Hit**: Return cached result without database query
### What Gets Cached
- **Cached**: All SELECT queries with `cacheTTL > 0`
- **Not Cached**: INSERT, UPDATE, DELETE, EXEC operations
- **Not Cached**: SELECT queries with `cacheTTL = 0`
### Cache Behavior
```go
// No caching (TTL = 0)
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
// Cache for 5 minutes
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 5*time.Minute)
// Cache for 1 hour
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = 1", time.Hour)
```
## Cache Types
### 1. In-Memory Weak Cache
**Type**: Local, process-specific, GC-managed
**Use Case**: Single-server applications, development, testing
**Characteristics:**
- Fastest access (no network)
- Memory managed by Go GC
- Weak pointers - automatically freed when under memory pressure
- Not shared across processes
- Lost on restart
**Setup:**
```go
db.UseCache(mysql.NewWeakCache())
```
**Pros:**
- Zero configuration
- No external dependencies
- Automatic memory management
- Extremely fast
**Cons:**
- Not shared across servers
- No distributed locking
- Cache lost on restart
- Memory limited
**Best For:**
- Development
- Testing
- Single-server deployments
- Applications with low cache requirements
### 2. Redis Cache
**Type**: Distributed, persistent
**Use Case**: Multi-server deployments, high-traffic applications
**Characteristics:**
- Shared across all application instances
- Distributed locking to prevent cache stampedes
- Configurable persistence
- Network latency overhead
- Requires Redis server
**Setup:**
```go
import "github.com/redis/go-redis/v9"
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
db.EnableRedis(redisClient)
```
**Pros:**
- Shared cache across servers
- Distributed locking
- Persistent (optional)
- High capacity
- Cache stampede prevention
**Cons:**
- Network latency
- Requires Redis infrastructure
- More complex setup
**Best For:**
- Production multi-server deployments
- High-traffic applications
- Applications requiring cache consistency
- Preventing thundering herd problems
### 3. Memcached Cache
**Type**: Distributed, volatile
**Use Case**: Multi-server deployments, simple caching needs
**Characteristics:**
- Shared across all application instances
- No persistence
- Simple protocol
- No distributed locking
- Requires Memcached server
**Setup:**
```go
import "github.com/bradfitz/gomemcache/memcache"
memcacheClient := memcache.New("localhost:11211")
db.EnableMemcache(memcacheClient)
```
**Pros:**
- Shared cache across servers
- Simple and fast
- Mature technology
- Good performance
**Cons:**
- No distributed locking
- No persistence
- Cache lost on restart
- No cache stampede prevention
**Best For:**
- Legacy infrastructure with Memcached
- Simple caching needs
- When distributed locking not required
## Cache Configuration
### Basic Setup
```go
// In-memory cache
db := mysql.New(...)
db.UseCache(mysql.NewWeakCache())
// Redis cache
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
db.EnableRedis(redisClient)
// Memcached
memcacheClient := memcache.New("localhost:11211")
db.EnableMemcache(memcacheClient)
```
### Redis Advanced Configuration
```go
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "secret",
DB: 0,
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
PoolSize: 10,
MinIdleConns: 5,
})
db.EnableRedis(redisClient)
```
### Redis Cluster
```go
redisClient := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{
"localhost:7000",
"localhost:7001",
"localhost:7002",
},
})
db.EnableRedis(redisClient)
```
### Environment Configuration
Configure cache behavior via environment variables:
```bash
# Redis lock retry delay (default: 0.020 seconds)
export COOL_REDIS_LOCK_RETRY_DELAY=0.050
# Max query execution time (default: 27 seconds)
export COOL_MAX_EXECUTION_TIME_TIME=30s
# Max retry attempts (default: unlimited)
export COOL_MAX_ATTEMPTS=5
```
## TTL Selection
### TTL Guidelines
Choose TTL based on data volatility and access patterns:
| Data Type | Recommended TTL | Rationale |
|-----------|----------------|-----------|
| User sessions | 5-15 minutes | Frequently changing |
| Reference data | 1-24 hours | Rarely changing |
| Analytics/Reports | 15-60 minutes | Tolerates staleness |
| Real-time data | 0 (no cache) | Must be fresh |
| Configuration | 5-60 minutes | Infrequent changes |
| Search results | 1-5 minutes | Balance freshness/load |
| Product catalogs | 10-30 minutes | Moderate change rate |
### Dynamic TTL Selection
```go
// Choose TTL based on query type
func getCacheTTL(queryType string) time.Duration {
switch queryType {
case "user_profile":
return 10 * time.Minute
case "product_catalog":
return 30 * time.Minute
case "analytics":
return time.Hour
case "real_time":
return 0 // No caching
default:
return 5 * time.Minute
}
}
var users []User
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = 1",
getCacheTTL("user_profile"))
```
### Conditional TTL
```go
// Cache differently based on result size
var users []User
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `status` = @@status", 0,
status)
ttl := 5 * time.Minute
if len(users) > 1000 {
// Large result set - cache longer to reduce load
ttl = 30 * time.Minute
}
// Re-query with caching
err = db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `status` = @@status", ttl,
status)
```
## Multi-Level Caching
### Layered Cache Strategy
Combine fast local cache with shared distributed cache:
```go
db.UseCache(mysql.NewMultiCache(
mysql.NewWeakCache(), // L1: Fast local cache
mysql.NewRedisCache(redisClient), // L2: Shared distributed cache
))
```
**How It Works:**
1. Check L1 (local weak cache) - fastest
2. If miss, check L2 (Redis) - shared
3. If miss, query database
4. Store result in both L1 and L2
**Benefits:**
- Extremely fast for repeated queries in same process
- Shared cache prevents duplicate work across servers
- Best of both worlds: speed + consistency
### Custom Multi-Level Configuration
```go
// Create custom cache layers
type CustomCache struct {
layers []mysql.Cache
}
func (c *CustomCache) Get(key string) ([]byte, bool) {
for _, layer := range c.layers {
if val, ok := layer.Get(key); ok {
// Backfill previous layers
for _, prevLayer := range c.layers {
if prevLayer == layer {
break
}
prevLayer.Set(key, val, 0)
}
return val, true
}
}
return nil, false
}
func (c *CustomCache) Set(key string, val []byte, ttl time.Duration) {
for _, layer := range c.layers {
layer.Set(key, val, ttl)
}
}
// Use custom cache
cache := &CustomCache{
layers: []mysql.Cache{
mysql.NewWeakCache(),
mysql.NewRedisCache(redis1),
mysql.NewRedisCache(redis2), // Backup Redis
},
}
db.UseCache(cache)
```
## Cache Invalidation
### Automatic Invalidation
cool-mysql doesn't auto-invalidate on writes. You must handle invalidation explicitly.
### Handling Cache After Writes
**Note:** Cache keys are generated internally by `cool-mysql` using SHA256 hashing and are not exposed as a public API. You cannot manually invalidate specific cache entries.
#### Recommended Pattern: Use SelectWrites
```go
// Write to database
err := db.Insert("users", user)
if err != nil {
return err
}
// Read from write pool for immediate consistency
err = db.SelectWrites(&user,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id",
0, // Don't cache write-pool reads
user.ID)
```
#### 3. Tag-Based Invalidation
```go
// Tag queries with invalidation keys
const userCacheTag = "users:all"
// Set cache with tag
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 10*time.Minute)
redisClient.SAdd(ctx, userCacheTag, cacheKey)
// Invalidate all user queries
keys, _ := redisClient.SMembers(ctx, userCacheTag).Result()
redisClient.Del(ctx, keys...)
redisClient.Del(ctx, userCacheTag)
```
#### 4. TTL-Based Invalidation
```go
// Rely on short TTL for eventual consistency
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`",
30*time.Second) // Short TTL = frequent refresh
```
### Cache Invalidation Strategies
| Strategy | Pros | Cons | Best For |
|----------|------|------|----------|
| Manual invalidation | Precise control | Complex to implement | Critical data |
| SelectWrites | Simple, consistent | Bypasses read pool | Read-after-write |
| Short TTL | Simple, automatic | Higher DB load | Frequently changing data |
| Tag-based | Bulk invalidation | Requires Redis | Related queries |
## Distributed Locking
### Cache Stampede Problem
When cache expires on high-traffic query:
1. Multiple requests see cache miss
2. All execute same expensive query simultaneously
3. Database overload
### Redis Distributed Locking Solution
cool-mysql's Redis cache includes distributed locking:
```go
db.EnableRedis(redisClient)
// Automatic distributed locking
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `active` = 1", 10*time.Minute)
```
**How It Works:**
1. First request gets lock, executes query
2. Subsequent requests wait for lock
3. First request populates cache
4. Waiting requests get result from cache
5. Lock automatically released
### Lock Configuration
```bash
# Configure lock retry delay
export COOL_REDIS_LOCK_RETRY_DELAY=0.020 # 20ms between retries
```
### Without Distributed Locking (Memcached)
Memcached doesn't support distributed locking. Mitigate stampedes with:
1. **Probabilistic Early Expiration**
```go
// Refresh cache before expiration
func shouldRefresh(ttl time.Duration) bool {
// Refresh 10% of requests in last 10% of TTL
return rand.Float64() < 0.1
}
```
2. **Stale-While-Revalidate**
```go
// Serve stale data while refreshing
// (Requires custom cache implementation)
```
## Performance Optimization
### Cache Hit Rate Monitoring
```go
type CacheStats struct {
Hits int64
Misses int64
}
var stats CacheStats
// Wrap cache to track stats
type StatsCache struct {
underlying mysql.Cache
stats *CacheStats
}
func (c *StatsCache) Get(key string) ([]byte, bool) {
val, ok := c.underlying.Get(key)
if ok {
atomic.AddInt64(&c.stats.Hits, 1)
} else {
atomic.AddInt64(&c.stats.Misses, 1)
}
return val, ok
}
// Use stats cache
statsCache := &StatsCache{
underlying: mysql.NewRedisCache(redisClient),
stats: &stats,
}
db.UseCache(statsCache)
// Check cache performance
hitRate := float64(stats.Hits) / float64(stats.Hits + stats.Misses)
fmt.Printf("Cache hit rate: %.2f%%\n", hitRate*100)
```
### Optimizing Cache Keys
cool-mysql generates cache keys from query + parameters. Optimize by:
1. **Normalizing Queries**
```go
// BAD: Different queries, same intent
db.Select(&users, "SELECT * FROM `users` WHERE `id` = @@id", ttl, params)
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id", ttl, params)
// ^ Different cache keys due to whitespace
// GOOD: Consistent formatting
const userByIDQuery = "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id"
db.Select(&users, userByIDQuery, ttl, params)
```
2. **Parameter Ordering**
```go
// Parameter order doesn't matter - they're normalized
db.Select(&users, query, ttl,
mysql.Params{"status": "active", "age": 18})
db.Select(&users, query, ttl,
mysql.Params{"age": 18, "status": "active"})
// ^ Same cache key
```
### Memory Usage Optimization
```go
// For memory-constrained environments
// Use shorter TTLs to reduce memory usage
db.UseCache(mysql.NewWeakCache())
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`",
1*time.Minute) // Short TTL = less memory
// Or use Redis with maxmemory policy
// redis.conf:
// maxmemory 100mb
// maxmemory-policy allkeys-lru
```
### Network Latency Optimization
```go
// Minimize Redis roundtrips with pipelining
// (Requires custom cache implementation)
// Or use MultiCache for local-first
db.UseCache(mysql.NewMultiCache(
mysql.NewWeakCache(), // Fast local first
mysql.NewRedisCache(redisClient), // Fallback to Redis
))
```
## Best Practices
### 1. Match TTL to Data Volatility
```go
// Frequently changing - short TTL or no cache
db.Select(&liveData, query, 0)
// Rarely changing - long TTL
db.Select(&refData, query, 24*time.Hour)
```
### 2. Use SelectWrites After Writes
```go
// Write
db.Insert("users", user)
// Read with consistency
db.SelectWrites(&user, query, 0, params)
```
### 3. Cache High-Traffic Queries
```go
// Identify expensive queries
// Use longer TTLs for high-traffic, expensive queries
db.Select(&results, expensiveQuery, 30*time.Minute)
```
### 4. Don't Over-Cache
```go
// Don't cache everything - adds complexity
// Only cache queries that benefit from caching:
// - Expensive to compute
// - Frequently accessed
// - Tolerates staleness
```
### 5. Monitor Cache Performance
```go
// Track hit rates
// Tune TTLs based on metrics
// Remove caching from low-hit-rate queries
```
### 6. Use MultiCache for Best Performance
```go
// Production setup
db.UseCache(mysql.NewMultiCache(
mysql.NewWeakCache(), // L1: Fast
mysql.NewRedisCache(redisClient), // L2: Shared
))
```
### 7. Handle Cache Failures Gracefully
```go
// Cache failures should fallback to database
// cool-mysql handles this automatically
// Even if Redis is down, queries still work
```
### 8. Consider Cache Warming
```go
// Pre-populate cache for known hot queries
func warmCache(db *mysql.Database) {
db.Select(&refData, "SELECT `id`, `name`, `code` FROM `countries`", 24*time.Hour)
db.Select(&config, "SELECT `key`, `value` FROM `config`", time.Hour)
}
```
### 9. Use Appropriate Cache for Environment
```go
// Development
db.UseCache(mysql.NewWeakCache())
// Production
db.EnableRedis(redisClient) // Distributed locking + sharing
```
### 10. Document Cache TTLs
```go
const (
// Cache TTLs
UserProfileTTL = 10 * time.Minute // User data changes moderately
ProductCatalogTTL = 30 * time.Minute // Products updated infrequently
AnalyticsTTL = time.Hour // Analytics can be stale
NoCache = 0 // Real-time data
)
db.Select(&user, query, UserProfileTTL, params)
```
## Troubleshooting
### High Cache Miss Rate
**Symptoms**: Low hit rate, high database load
**Solutions:**
- Increase TTL
- Check if queries are identical (whitespace, parameter names)
- Verify cache is configured correctly
- Check if queries are actually repeated
### Cache Stampede
**Symptoms**: Periodic database spikes, slow response during cache expiration
**Solutions:**
- Use Redis with distributed locking
- Implement probabilistic early refresh
- Increase TTL to reduce expiration frequency
### Memory Issues
**Symptoms**: High memory usage, OOM errors
**Solutions:**
- Reduce TTLs
- Use Redis instead of in-memory
- Configure Redis maxmemory policy
- Cache fewer queries
### Stale Data
**Symptoms**: Users see outdated information
**Solutions:**
- Reduce TTL
- Use SelectWrites after modifications
- Implement cache invalidation
- Consider if data should be cached at all

View File

@@ -0,0 +1,794 @@
# Query Patterns Guide
Practical examples and patterns for common cool-mysql query scenarios.
## Table of Contents
1. [Basic SELECT Patterns](#basic-select-patterns)
2. [Named Parameters](#named-parameters)
3. [Template Syntax](#template-syntax)
4. [Result Mapping](#result-mapping)
5. [Streaming with Channels](#streaming-with-channels)
6. [Function Receivers](#function-receivers)
7. [JSON Handling](#json-handling)
8. [Complex Queries](#complex-queries)
9. [Raw SQL](#raw-sql)
## Basic SELECT Patterns
### Select into Struct Slice
```go
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Email string `mysql:"email"`
CreatedAt time.Time `mysql:"created_at"`
}
var users []User
err := db.Select(&users,
"SELECT `id`, `name`, `email`, created_at FROM `users` WHERE age > @@minAge",
5*time.Minute, // Cache for 5 minutes
18)
```
### Select Single Struct
```go
var user User
err := db.Select(&user,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id",
0, // No caching
123)
if errors.Is(err, sql.ErrNoRows) {
// User not found
return fmt.Errorf("user not found")
}
```
### Select Single Value
```go
// String value
var name string
err := db.Select(&name,
"SELECT `name` FROM `users` WHERE `id` = @@id",
0,
123)
// Integer value
var count int
err := db.Select(&count,
"SELECT COUNT(*) FROM `users` WHERE `active` = @@active",
0,
1)
// Time value
var lastLogin time.Time
err := db.Select(&lastLogin,
"SELECT last_login FROM `users` WHERE `id` = @@id",
0,
123)
```
### Select Multiple Values (First Row Only)
```go
type UserInfo struct {
Name string
Email string
Age int
}
var info UserInfo
err := db.Select(&info,
"SELECT name, `email`, `age` FROM `users` WHERE `id` = @@id",
0,
123)
```
## Named Parameters
### Basic Parameter Usage
```go
// Simple parameters
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `status` = @@status",
0,
mysql.Params{"minAge": 18, "status": "active"})
```
### Struct as Parameters
```go
// Use struct fields as parameters
filter := struct {
MinAge int
Status string
City string
}{
MinAge: 18,
Status: "active",
City: "New York",
}
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@MinAge AND `status` = @@Status AND city = @@City",
0,
filter)
```
### Multiple Parameter Sources
```go
// Parameters are merged (last wins for duplicates)
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `status` = @@status AND city = @@city",
0,
mysql.Params{"minAge": 18, "status": "active"},
mysql.Params{"city": "New York"},
)
```
### Parameter Reuse
```go
// Same parameter used multiple times
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`"+
" WHERE (`age` BETWEEN @@minAge AND @@maxAge)"+
" AND (`created_at` > @@date OR `updated_at` > @@date)",
0,
mysql.Params{
"minAge": 18,
"maxAge": 65,
"date": time.Now().Add(-7*24*time.Hour),
})
```
### Case-Insensitive Parameter Merging
```go
// These parameters are treated as the same (normalized to lowercase)
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `name` = @@userName",
0,
mysql.Params{"username": "Alice"}, // lowercase 'u'
mysql.Params{"UserName": "Bob"}, // uppercase 'U' - this wins
)
// Effective parameter: "Bob"
```
## Template Syntax
### Conditional Query Parts
```go
// Add WHERE conditions dynamically
params := mysql.Params{
"minAge": 18,
"status": "active",
}
query := `
SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`
WHERE 1=1
{{ if .MinAge }}AND `age` > @@minAge{{ end }}
{{ if .Status }}AND `status` = @@status{{ end }}
`
var users []User
err := db.Select(&users, query, 0, params)
```
### Dynamic ORDER BY
```go
// For dynamic ORDER BY, validate column names (identifiers can't be marshaled)
type QueryParams struct {
SortBy string
SortOrder string
}
// Whitelist allowed columns for safety
allowedColumns := map[string]bool{
"created_at": true,
"name": true,
"email": true,
}
params := QueryParams{
SortBy: "created_at",
SortOrder: "DESC",
}
// Validate before using
if !allowedColumns[params.SortBy] {
return errors.New("invalid sort column")
}
allowedOrders := map[string]bool{"ASC": true, "DESC": true}
if !allowedOrders[params.SortOrder] {
return errors.New("invalid sort order")
}
// Now safe to inject validated identifiers
query := `
SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`
WHERE `active` = 1
{{ if .SortBy }}
ORDER BY {{ .SortBy }} {{ .SortOrder }}
{{ end }}
`
var users []User
err := db.Select(&users, query, 0, params)
```
### Conditional JOINs
```go
type SearchParams struct {
IncludeOrders bool
IncludeAddress bool
}
params := SearchParams{
IncludeOrders: true,
IncludeAddress: false,
}
query := "SELECT `users`.`id`, `users`.`name`, `users`.`email`, `users`.`age`, `users`.`active`, `users`.`created_at`, `users`.`updated_at`" +
" {{ if .IncludeOrders }}, subquery.order_count{{ end }}" +
" {{ if .IncludeAddress }}, `addresses`.`city`{{ end }}" +
" FROM `users`" +
" {{ if .IncludeOrders }}" +
" LEFT JOIN (" +
" SELECT `user_id`, COUNT(*) as `order_count`" +
" FROM `orders`" +
" GROUP BY `user_id`" +
" ) subquery ON `users`.`id` = subquery.`user_id`" +
" {{ end }}" +
" {{ if .IncludeAddress }}" +
" LEFT JOIN `addresses` ON `users`.`id` = `addresses`.`user_id`" +
" {{ end }}"
var users []User
err := db.Select(&users, query, 0, params)
```
### Template with Custom Functions
```go
// Add custom template functions
db.AddTemplateFuncs(template.FuncMap{
"upper": strings.ToUpper,
"quote": func(s string) string { return fmt.Sprintf("'%s'", s) },
})
// Use in query
query := `
SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`
WHERE `status` = {{ quote (upper .Status) }}
`
err := db.Select(&users, query, 0, "active")
// Generates: WHERE `status` = 'ACTIVE'
```
### Template Best Practices
```go
// DON'T: Use column names from tags in templates
type User struct {
Username string `mysql:"user_name"` // Column is "user_name"
}
// WRONG - uses column name
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` {{ if .user_name }}WHERE `name` = @@name{{ end }}"
// CORRECT - uses field name
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` {{ if .Username }}WHERE `name` = @@name{{ end }}"
```
## Result Mapping
### Map Results
```go
// Single row as map
var row mysql.MapRow
err := db.Select(&row,
"SELECT `id`, `name`, `email` FROM `users` WHERE `id` = @@id",
0,
123)
fmt.Printf("Name: %v\n", row["name"])
// Multiple rows as maps
var rows mysql.MapRows
err := db.Select(&rows,
"SELECT `id`, `name`, `email` FROM `users`",
0)
for _, row := range rows {
fmt.Printf("ID: %v, Name: %v\n", row["id"], row["name"])
}
```
### Slice Results
```go
// Single row as slice
var row mysql.SliceRow
err := db.Select(&row,
"SELECT `id`, `name`, `email` FROM `users` WHERE `id` = @@id",
0,
123)
fmt.Printf("First column: %v\n", row[0])
// Multiple rows as slices
var rows mysql.SliceRows
err := db.Select(&rows,
"SELECT `id`, `name`, `email` FROM `users`",
0)
for _, row := range rows {
fmt.Printf("Row: %v\n", row)
}
```
### Partial Struct Mapping
```go
// Struct with subset of columns
type UserSummary struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
}
var summaries []UserSummary
err := db.Select(&summaries,
"SELECT `id`, name FROM `users`",
0)
```
### Embedded Structs
```go
type Timestamps struct {
CreatedAt time.Time `mysql:"created_at"`
UpdatedAt time.Time `mysql:"updated_at"`
}
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Email string `mysql:"email"`
Timestamps
}
var users []User
err := db.Select(&users,
"SELECT `id`, `name`, `email`, created_at, updated_at FROM `users`",
0)
```
### Pointer Fields
```go
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Email *string `mysql:"email"` // NULL-able
LastLogin *time.Time `mysql:"last_login"` // NULL-able
}
var users []User
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
for _, user := range users {
if user.Email != nil {
fmt.Printf("Email: %s\n", *user.Email)
}
if user.LastLogin != nil {
fmt.Printf("Last login: %v\n", *user.LastLogin)
}
}
```
## Streaming with Channels
### Select into Channel
```go
// Stream results to avoid loading all into memory
userCh := make(chan User, 100) // Buffered channel
go func() {
defer close(userCh)
if err := db.Select(userCh, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0); err != nil {
log.Printf("Select error: %v", err)
}
}()
// Process as they arrive
for user := range userCh {
if err := processUser(user); err != nil {
log.Printf("Process error: %v", err)
}
}
```
### Insert from Channel
```go
// Stream inserts to avoid building large slice
userCh := make(chan User, 100)
// Producer
go func() {
defer close(userCh)
for i := 0; i < 10000; i++ {
userCh <- User{
Name: fmt.Sprintf("User %d", i),
Email: fmt.Sprintf("user%d@example.com", i),
}
}
}()
// Consumer - automatically chunks and inserts
if err := db.Insert("users", userCh); err != nil {
log.Printf("Insert error: %v", err)
}
```
### Channel with Context
```go
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
userCh := make(chan User, 100)
go func() {
defer close(userCh)
db.SelectContext(ctx, userCh, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
}()
for user := range userCh {
select {
case <-ctx.Done():
log.Println("Timeout reached")
return ctx.Err()
default:
processUser(user)
}
}
```
## Function Receivers
### Basic Function Receiver
```go
// Process each row with function
err := db.Select(func(u User) {
log.Printf("Processing user: %s (%s)", u.Name, u.Email)
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
```
### Function Receiver with Error Handling
```go
// Return error to stop iteration
var processErr error
err := db.Select(func(u User) {
if err := validateUser(u); err != nil {
processErr = err
return
}
processUser(u)
}, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
if err != nil {
return err
}
if processErr != nil {
return processErr
}
```
### Aggregation with Function Receiver
```go
// Collect aggregate data
var totalAge int
var count int
err := db.Select(func(u User) {
totalAge += u.Age
count++
}, "SELECT age FROM `users` WHERE `active` = 1", 0)
if count > 0 {
avgAge := float64(totalAge) / float64(count)
fmt.Printf("Average age: %.2f\n", avgAge)
}
```
## JSON Handling
### JSON Column to Struct Field
```go
type UserMeta struct {
Preferences map[string]any `json:"preferences"`
Settings map[string]any `json:"settings"`
}
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Meta UserMeta `mysql:"meta"` // JSON column
}
var users []User
err := db.Select(&users,
"SELECT `id`, `name`, meta FROM `users`",
0)
for _, user := range users {
fmt.Printf("Preferences: %+v\n", user.Meta.Preferences)
}
```
### Select as JSON
```go
var result json.RawMessage
err := db.SelectJSON(&result,
`SELECT JSON_OBJECT(
'id', id,
'name', `name`,
'email', `email`
) FROM `users` WHERE `id` = @@id`,
0,
123)
fmt.Printf("JSON: %s\n", string(result))
```
### JSON Array Results
```go
var results json.RawMessage
err := db.SelectJSON(&results,
`SELECT JSON_ARRAYAGG(
JSON_OBJECT(
'id', id,
'name', name
)
) FROM users`,
0)
```
## Complex Queries
### Subqueries with Named Parameters
```go
query := "SELECT `users`.`id`, `users`.`name`, `users`.`email`, `users`.`age`, `users`.`active`, `users`.`created_at`, `users`.`updated_at`," +
" (SELECT COUNT(*) FROM `orders` WHERE `orders`.`user_id` = `users`.`id`) as `order_count`" +
" FROM `users`" +
" WHERE `users`.`created_at` > @@since" +
" AND `users`.`status` = @@status"
var users []struct {
User
OrderCount int `mysql:"order_count"`
}
err := db.Select(&users, query, 5*time.Minute,
mysql.Params{
"since": time.Now().Add(-30*24*time.Hour),
"status": "active",
})
```
### JOINs with Named Parameters
```go
query := "SELECT" +
" `users`.`id`," +
" `users`.`name`," +
" `users`.`email`," +
" `orders`.`order_id`," +
" `orders`.`total`" +
" FROM `users`" +
" INNER JOIN `orders` ON `users`.`id` = `orders`.`user_id`" +
" WHERE `users`.`status` = @@status" +
" AND `orders`.`created_at` > @@since" +
" AND `orders`.`total` > @@minTotal" +
" ORDER BY `orders`.`created_at` DESC"
type UserOrder struct {
UserID int `mysql:"id"`
Name string `mysql:"name"`
Email string `mysql:"email"`
OrderID int `mysql:"order_id"`
Total float64 `mysql:"total"`
}
var results []UserOrder
err := db.Select(&results, query, 0,
mysql.Params{
"status": "active",
"since": time.Now().Add(-7*24*time.Hour),
"minTotal": 100.0,
})
```
### IN Clause with Multiple Values
```go
// cool-mysql natively supports slices - just pass them directly!
ids := []int{1, 2, 3, 4, 5}
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` IN (@@ids)"
var users []User
err := db.Select(&users, query, 0,
ids)
// Automatically expands to: SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` IN (1,2,3,4,5)
// Works with any slice type
emails := []string{"user1@example.com", "user2@example.com"}
err = db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` IN (@@emails)", 0,
emails)
// For very large lists (10,000+ items), consider JSON_TABLE (MySQL 8.0+)
// This can be more efficient than IN clause with many values
var largeIDs []int // thousands of IDs
idsJSON, _ := json.Marshal(largeIDs)
query = "SELECT `users`.`id`, `users`.`name`, `users`.`email`, `users`.`age`, `users`.`active`, `users`.`created_at`, `users`.`updated_at`" +
" FROM `users`" +
" JOIN JSON_TABLE(" +
" @@ids," +
" '$[*]' COLUMNS(`id` INT PATH '$')" +
" ) AS json_ids ON `users`.`id` = json_ids.`id`"
err = db.Select(&users, query, 0,
string(idsJSON))
```
### Window Functions
```go
query := "SELECT" +
" `id`," +
" `name`," +
" `salary`," +
" RANK() OVER (ORDER BY `salary` DESC) as `salary_rank`," +
" AVG(`salary`) OVER () as `avg_salary`" +
" FROM `employees`" +
" WHERE `department` = @@dept"
type EmployeeStats struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Salary float64 `mysql:"salary"`
SalaryRank int `mysql:"salary_rank"`
AvgSalary float64 `mysql:"avg_salary"`
}
var stats []EmployeeStats
err := db.Select(&stats, query, 5*time.Minute,
"Engineering")
```
## Raw SQL
### Literal SQL Injection
```go
// Use Raw() for SQL that shouldn't be escaped
// WARNING: Never use with user input - SQL injection risk!
query := `
SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`
WHERE @@dynamicCondition
AND `status` = @@status
`
err := db.Select(&users, query, 0,
mysql.Params{
"dynamicCondition": mysql.Raw("created_at > NOW() - INTERVAL 7 DAY"),
"status": "active", // This IS escaped
})
```
### Dynamic Table Names
```go
// Table names can't be parameterized - use fmt.Sprintf carefully
tableName := "users" // Validate this!
query := fmt.Sprintf("SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM %s WHERE `status` = @@status",
tableName) // Ensure tableName is validated/sanitized!
var users []User
err := db.Select(&users, query, 0,
"active")
```
### CASE Statements with Raw
```go
query := `
SELECT
id,
name,
@@statusCase as `status_label`
FROM `users`
`
statusCase := mysql.Raw(`
CASE status
WHEN 1 THEN 'Active'
WHEN 2 THEN 'Inactive'
WHEN 3 THEN 'Suspended'
ELSE 'Unknown'
END
`)
type UserWithLabel struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
StatusLabel string `mysql:"status_label"`
}
var users []UserWithLabel
err := db.Select(&users, query, 0,
statusCase)
```
## Debugging Queries
### Inspect Interpolated Query
```go
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `status` = @@status"
params := mysql.Params{"minAge": 18, "status": "active"}
replacedQuery, normalizedParams, err := db.InterpolateParams(query, params)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Query: %s\n", replacedQuery)
fmt.Printf("Params: %+v\n", normalizedParams)
// Query: SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > ? AND `status` = ?
// Params: [18 active]
```
### Log Query Execution
```go
// Set up query logging
db.SetQueryLogger(func(query string, args []any, duration time.Duration, err error) {
log.Printf("[%v] %s %+v (err: %v)", duration, query, args, err)
})
// Now all queries will be logged
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@age", 0,
18)
```
## Performance Tips
1. **Use caching for expensive queries**: Set appropriate TTL based on data volatility
2. **Stream large result sets**: Use channels instead of loading all into memory
3. **Batch inserts**: Use slices or channels instead of individual inserts
4. **Use SelectWrites sparingly**: Only when you need read-after-write consistency
5. **Index your parameters**: Ensure WHERE clause columns are indexed
6. **Avoid SELECT ***: Specify only columns you need for better performance
7. **Use templates wisely**: Don't overcomplicate queries - keep them readable
8. **Monitor cache hit rates**: Tune TTLs based on actual hit rates

652
references/struct-tags.md Normal file
View File

@@ -0,0 +1,652 @@
# Struct Tags Reference
Complete guide to struct tag usage in cool-mysql for controlling column mapping and behavior.
## Table of Contents
1. [Basic Tag Syntax](#basic-tag-syntax)
2. [Tag Options](#tag-options)
3. [Default Value Handling](#default-value-handling)
4. [Special Characters](#special-characters)
5. [Custom Interfaces](#custom-interfaces)
6. [Advanced Patterns](#advanced-patterns)
7. [Common Gotchas](#common-gotchas)
## Basic Tag Syntax
### Column Mapping
```go
type User struct {
ID int `mysql:"id"` // Maps to 'id' column
Name string `mysql:"name"` // Maps to 'name' column
Email string `mysql:"email"` // Maps to 'email' column
}
```
**Default Behavior (No Tag):**
```go
type User struct {
ID int // Maps to 'ID' column (exact field name)
Name string // Maps to 'Name' column
}
```
### Multiple Tags
```go
type User struct {
ID int `mysql:"id" json:"id"`
Name string `mysql:"name" json:"name"`
CreatedAt time.Time `mysql:"created_at" json:"created_at"`
}
```
## Tag Options
### Available Options
| Option | Syntax | Behavior |
|--------|--------|----------|
| Column name | `mysql:"column_name"` | Maps to specific column |
| Default zero | `mysql:"column_name,defaultzero"` | Use DEFAULT() for zero values |
| Omit empty | `mysql:"column_name,omitempty"` | Same as `defaultzero` |
| Insert default | `mysql:"column_name,insertDefault"` | Same as `defaultzero` |
| Ignore field | `mysql:"-"` | Completely ignore field |
### Column Name Only
```go
type User struct {
UserID int `mysql:"id"` // Field name differs from column name
}
// INSERT INTO `users` (id) VALUES (?)
```
### defaultzero Option
```go
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
CreatedAt time.Time `mysql:"created_at,defaultzero"`
}
// If CreatedAt.IsZero():
// INSERT INTO `users` (id, `name`, created_at) VALUES (?, ?, DEFAULT(created_at))
// Else:
// INSERT INTO `users` (id, `name`, created_at) VALUES (?, ?, ?)
```
### omitempty Option
```go
type User struct {
ID int `mysql:"id"`
UpdatedAt time.Time `mysql:"updated_at,omitempty"`
}
// Equivalent to defaultzero
```
### insertDefault Option
```go
type User struct {
ID int `mysql:"id"`
CreatedAt time.Time `mysql:"created_at,insertDefault"`
}
// Equivalent to defaultzero
```
### Ignore Field
```go
type User struct {
ID int `mysql:"id"`
Password string `mysql:"-"` // Never included in queries
internal string // Unexported fields also ignored
}
// INSERT INTO `users` (id) VALUES (?)
// Password is never inserted or selected
```
## Default Value Handling
### When to Use defaultzero
Use `defaultzero` when:
- Database column has a DEFAULT value
- You want to use database default for zero values
- Common for timestamps with `DEFAULT CURRENT_TIMESTAMP`
### Database Setup
```sql
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
### Struct Definition
```go
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
CreatedAt time.Time `mysql:"created_at,defaultzero"`
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
}
```
### Usage
```go
// CreatedAt and UpdatedAt are zero values
user := User{
Name: "Alice",
}
db.Insert("users", user)
// INSERT INTO `users` (name, created_at, updated_at)
// VALUES (?, DEFAULT(created_at), DEFAULT(updated_at))
// Database sets timestamps automatically
```
### Zero Value Detection
**Built-in zero values:**
- `int`, `int64`, etc.: `0`
- `string`: `""`
- `bool`: `false`
- `time.Time`: `time.Time{}.IsZero()` returns `true`
- `*T` (pointers): `nil`
- `[]T` (slices): `nil` or `len == 0`
**Custom zero detection:**
Implement `Zeroer` interface (see [Custom Interfaces](#custom-interfaces))
## Special Characters
### Hex Encoding
For column names with special characters, use hex encoding:
```go
type Data struct {
// Column name: "column,name"
Value string `mysql:"column0x2cname"`
}
// 0x2c is hex for ','
```
### Common Hex Codes
| Character | Hex Code | Example |
|-----------|----------|---------|
| `,` | `0x2c` | `column0x2cname` |
| `:` | `0x3a` | `column0x3aname` |
| `"` | `0x22` | `column0x22name` |
| Space | `0x20` | `column0x20name` |
### Generating Hex Codes
```go
// Get hex code for character
char := ','
hexCode := fmt.Sprintf("0x%x", char)
fmt.Println(hexCode) // 0x2c
```
## Custom Interfaces
### Zeroer Interface
Implement custom zero-value detection:
```go
type Zeroer interface {
IsZero() bool
}
```
**Example:**
```go
type CustomTime struct {
time.Time
}
func (ct CustomTime) IsZero() bool {
// Consider Unix epoch (0) as zero
return ct.Time.IsZero() || ct.Time.Unix() == 0
}
type Event struct {
ID int `mysql:"id"`
Timestamp CustomTime `mysql:"timestamp,defaultzero"`
}
// If Timestamp.Unix() == 0:
// INSERT ... VALUES (..., DEFAULT(timestamp))
```
**Use Cases:**
- Custom "empty" definitions
- Sentinel values treated as zero
- Domain-specific zero logic
### Valueser Interface
Implement custom value conversion:
```go
type Valueser interface {
Values() []any
}
```
**Example:**
```go
type Point struct {
X, Y float64
}
func (p Point) Values() []any {
return []any{p.X, p.Y}
}
type Location struct {
ID int `mysql:"id"`
Position Point `mysql:"x,y"` // Note: two columns
}
// INSERT INTO locations (id, x, y) VALUES (?, ?, ?)
// Point.Values() returns [X, Y]
```
**Use Cases:**
- Mapping Go type to multiple columns
- Custom serialization
- Complex type conversion
## Advanced Patterns
### Embedded Structs
```go
type Timestamps struct {
CreatedAt time.Time `mysql:"created_at,defaultzero"`
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
}
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Timestamps // Embedded fields included
}
// SELECT `id`, `name`, created_at, updated_at FROM `users`
```
### Pointer Fields for NULL Handling
```go
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Email *string `mysql:"email"` // NULL-able
PhoneNumber *string `mysql:"phone_number"` // NULL-able
LastLogin *time.Time `mysql:"last_login"` // NULL-able
}
// Nil pointer = NULL in database
user := User{
ID: 1,
Name: "Alice",
Email: nil, // Will be NULL in database
}
db.Insert("users", user)
// INSERT INTO `users` (id, `name`, `email`, phone_number, last_login)
// VALUES (?, ?, NULL, NULL, NULL)
```
### Partial Struct Selects
```go
// Full struct
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Email string `mysql:"email"`
CreatedAt time.Time `mysql:"created_at"`
UpdatedAt time.Time `mysql:"updated_at"`
}
// Partial struct for specific query
type UserSummary struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
}
var summaries []UserSummary
db.Select(&summaries, "SELECT `id`, name FROM `users`", 0)
// Only maps id and name columns
```
### JSON Column Mapping
```go
type UserMeta struct {
Theme string `json:"theme"`
Preferences map[string]interface{} `json:"preferences"`
}
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Meta UserMeta `mysql:"meta"` // JSON column in MySQL
}
// cool-mysql automatically marshals/unmarshals JSON
db.Insert("users", User{
ID: 1,
Name: "Alice",
Meta: UserMeta{
Theme: "dark",
Preferences: map[string]interface{}{"notifications": true},
},
})
```
### Ignored Fields with Computed Values
```go
type User struct {
ID int `mysql:"id"`
FirstName string `mysql:"first_name"`
LastName string `mysql:"last_name"`
FullName string `mysql:"-"` // Computed, not in DB
}
var users []User
db.Select(&users, "SELECT `id`, first_name, last_name FROM `users`", 0)
// Compute FullName after query
for i := range users {
users[i].FullName = users[i].FirstName + " " + users[i].LastName
}
```
## Common Gotchas
### 1. Tag Takes Precedence Over Field Name
```go
type User struct {
UserID int `mysql:"id"` // Column is 'id', not 'UserID'
}
// Query must use actual column name
db.Select(&user, "SELECT id FROM `users` WHERE `id` = @@id", 0,
1)
```
### 2. Templates Use Field Names, Not Column Names
```go
type User struct {
Username string `mysql:"user_name"` // Column: user_name, Field: Username
}
// CORRECT - uses field name
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` {{ if .Username }}WHERE `user_name` = @@name{{ end }}"
// WRONG - user_name is column, not field
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` {{ if .user_name }}WHERE `user_name` = @@name{{ end }}"
```
### 3. Unexported Fields Are Ignored
```go
type User struct {
ID int `mysql:"id"`
name string `mysql:"name"` // Ignored - unexported
}
// Only ID is inserted
db.Insert("users", user)
```
### 4. Multiple Option Order Doesn't Matter
```go
// These are equivalent
`mysql:"column_name,defaultzero"`
`mysql:"column_name,omitempty"`
`mysql:"column_name,insertDefault"`
```
### 5. Embedded Struct Tag Conflicts
```go
type Base struct {
ID int `mysql:"id"`
}
type User struct {
Base
ID int `mysql:"user_id"` // Shadows Base.ID
}
// User.ID takes precedence
```
### 6. Zero Values vs NULL
```go
// Zero value != NULL
type User struct {
Age int `mysql:"age"` // 0 is inserted, not NULL
}
// Use pointer for NULL
type User struct {
Age *int `mysql:"age"` // nil = NULL, 0 = 0
}
```
### 7. defaultzero Doesn't Affect SELECT
```go
type User struct {
CreatedAt time.Time `mysql:"created_at,defaultzero"`
}
// defaultzero only affects INSERT/UPSERT, not SELECT
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
// CreatedAt is populated from database regardless of tag
```
## Tag Comparison
### json vs mysql Tags
```go
type User struct {
ID int `mysql:"id" json:"userId"` // DB: id, JSON: userId
Name string `mysql:"name" json:"name"` // Same for both
}
// MySQL: id, name
// JSON: userId, name
```
### When Tags Differ
```go
type User struct {
DatabaseID int `mysql:"id"` // Database column
UserName string `mysql:"username"` // Database column
ComputedRank int `mysql:"-"` // Not in database
}
// Database columns: id, username
// Struct fields: DatabaseID, UserName, ComputedRank
```
## Best Practices
### 1. Explicit Tags for Clarity
```go
// GOOD - explicit tags
type User struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
}
// OKAY - relies on field names matching columns
type User struct {
ID int
Name string
}
```
### 2. Use defaultzero for Timestamps
```go
type User struct {
CreatedAt time.Time `mysql:"created_at,defaultzero"`
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
}
```
### 3. Use Pointers for NULL-able Columns
```go
type User struct {
Email *string `mysql:"email"` // Can be NULL
LastLogin *time.Time `mysql:"last_login"` // Can be NULL
}
```
### 4. Ignore Computed Fields
```go
type User struct {
FirstName string `mysql:"first_name"`
LastName string `mysql:"last_name"`
FullName string `mysql:"-"` // Computed field
}
```
### 5. Document Custom Interfaces
```go
// CustomTime implements Zeroer for custom zero detection
type CustomTime struct {
time.Time
}
func (ct CustomTime) IsZero() bool {
return ct.Time.Unix() <= 0
}
```
### 6. Consistent Naming Convention
```go
// GOOD - consistent snake_case in tags
type User struct {
ID int `mysql:"id"`
FirstName string `mysql:"first_name"`
LastName string `mysql:"last_name"`
}
// AVOID - mixing conventions
type User struct {
ID int `mysql:"id"`
FirstName string `mysql:"firstName"` // camelCase
LastName string `mysql:"last_name"` // snake_case
}
```
### 7. Test Zero Value Behavior
```go
func TestUserInsertDefaults(t *testing.T) {
user := User{Name: "Alice"} // CreatedAt is zero
err := db.Insert("users", user)
// Verify database used DEFAULT value
}
```
## Examples
### Complete User Struct
```go
type User struct {
// Primary key
ID int `mysql:"id"`
// Basic fields
Email string `mysql:"email"`
Username string `mysql:"username"`
// NULL-able fields
FirstName *string `mysql:"first_name"`
LastName *string `mysql:"last_name"`
PhoneNumber *string `mysql:"phone_number"`
LastLogin *time.Time `mysql:"last_login"`
// Timestamps with defaults
CreatedAt time.Time `mysql:"created_at,defaultzero"`
UpdatedAt time.Time `mysql:"updated_at,defaultzero"`
// JSON column
Metadata json.RawMessage `mysql:"metadata"`
// Ignored fields
Password string `mysql:"-"` // Never persisted
PasswordHash string `mysql:"password_hash"`
}
```
### Product with Custom Types
```go
type Decimal struct {
Value *big.Float
}
func (d Decimal) Values() []any {
if d.Value == nil {
return []any{nil}
}
f, _ := d.Value.Float64()
return []any{f}
}
func (d Decimal) IsZero() bool {
return d.Value == nil || d.Value.Cmp(big.NewFloat(0)) == 0
}
type Product struct {
ID int `mysql:"id"`
Name string `mysql:"name"`
Price Decimal `mysql:"price,defaultzero"`
Description *string `mysql:"description"`
CreatedAt time.Time `mysql:"created_at,defaultzero"`
}
```

View File

@@ -0,0 +1,884 @@
# Testing Patterns Guide
Complete guide to testing applications that use cool-mysql, including mocking strategies and test patterns.
## Table of Contents
1. [Testing Strategies](#testing-strategies)
2. [Using sqlmock](#using-sqlmock)
3. [Test Database Setup](#test-database-setup)
4. [Testing Patterns](#testing-patterns)
5. [Context-Based Testing](#context-based-testing)
6. [Testing Caching](#testing-caching)
7. [Integration Testing](#integration-testing)
8. [Best Practices](#best-practices)
## Testing Strategies
### Three Approaches
| Approach | Pros | Cons | Best For |
|----------|------|------|----------|
| **sqlmock** | Fast, no DB needed, precise control | Manual setup, brittle | Unit tests |
| **Test Database** | Real MySQL behavior | Slower, requires DB | Integration tests |
| **In-Memory DB** | Fast, real SQL | Limited MySQL features | Quick tests |
### When to Use Each
**sqlmock:**
- Unit testing business logic
- Testing error handling
- CI/CD pipelines without database
- Rapid iteration
**Test Database:**
- Integration testing
- Testing complex queries
- Verifying MySQL-specific behavior
- End-to-end tests
**In-Memory (SQLite):**
- Quick local tests
- Testing SQL logic (not MySQL-specific)
- Prototyping
## Using sqlmock
### Setup
```go
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
mysqlDriver "github.com/go-sql-driver/mysql"
"github.com/StirlingMarketingGroup/cool-mysql"
)
func setupMockDB(t *testing.T) (*mysql.Database, sqlmock.Sqlmock) {
// Create mock SQL connection
mockDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Failed to create mock: %v", err)
}
// Create cool-mysql Database from mock connection
db, err := mysql.NewFromConn(mockDB, mockDB)
if err != nil {
t.Fatalf("Failed to create db: %v", err)
}
return db, mock
}
```
### Basic Select Test
```go
func TestSelectUsers(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
// Define expected query and result
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
AddRow(1, "Alice", "alice@example.com").
AddRow(2, "Bob", "bob@example.com")
mock.ExpectQuery("SELECT (.+) FROM `users` WHERE age > ?").
WithArgs(18).
WillReturnRows(rows)
// Execute query
var users []User
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
0,
18)
// Verify
if err != nil {
t.Errorf("Select failed: %v", err)
}
if len(users) != 2 {
t.Errorf("Expected 2 users, got %d", len(users))
}
if users[0].Name != "Alice" {
t.Errorf("Expected Alice, got %s", users[0].Name)
}
// Verify all expectations met
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
```
### Insert Test
```go
func TestInsertUser(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
user := User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
}
// Expect INSERT statement
mock.ExpectExec("INSERT INTO `users`").
WithArgs(1, "Alice", "alice@example.com").
WillReturnResult(sqlmock.NewResult(1, 1))
// Execute insert
err := db.Insert("users", user)
// Verify
if err != nil {
t.Errorf("Insert failed: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
```
### Update Test
```go
func TestUpdateUser(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
// Expect UPDATE statement
mock.ExpectExec("UPDATE `users` SET `name` = \\? WHERE `id` = \\?").
WithArgs("Alice Updated", 1).
WillReturnResult(sqlmock.NewResult(0, 1))
// Execute update
err := db.Exec("UPDATE `users` SET `name` = @@name WHERE `id` = @@id",
mysql.Params{"name": "Alice Updated", "id": 1})
// Verify
if err != nil {
t.Errorf("Update failed: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
```
### Error Handling Test
```go
func TestSelectError(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
// Expect query to return error
mock.ExpectQuery("SELECT (.+) FROM `users`").
WillReturnError(sql.ErrNoRows)
// Execute query
var user User
err := db.Select(&user, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id", 0,
999)
// Verify error returned
if !errors.Is(err, sql.ErrNoRows) {
t.Errorf("Expected sql.ErrNoRows, got %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
```
### Transaction Test
```go
func TestTransaction(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
// Expect transaction
mock.ExpectBegin()
mock.ExpectExec("INSERT INTO `users`").
WithArgs(1, "Alice", "alice@example.com").
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// Execute transaction
ctx := context.Background()
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
defer cancel()
if err != nil {
t.Fatalf("Failed to create tx: %v", err)
}
// Store transaction in context so operations use it
ctx = mysql.NewContextWithTx(ctx, tx)
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
err = db.Insert("users", user)
if err != nil {
t.Errorf("Insert failed: %v", err)
}
if err := commit(); err != nil {
t.Errorf("Commit failed: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
```
## Test Database Setup
### Docker Compose Setup
```yaml
# docker-compose.test.yml
version: '3.8'
services:
mysql-test:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: testpass
MYSQL_DATABASE: testdb
ports:
- "3307:3306"
tmpfs:
- /var/lib/mysql # In-memory for speed
```
### Test Helper
```go
// testutil/db.go
package testutil
import (
"database/sql"
"testing"
"github.com/StirlingMarketingGroup/cool-mysql"
)
func SetupTestDB(t *testing.T) *mysql.Database {
db, err := mysql.New(
"root", "testpass", "testdb", "localhost", 3307,
"root", "testpass", "testdb", "localhost", 3307,
"utf8mb4_unicode_ci",
"UTC",
)
if err != nil {
t.Fatalf("Failed to connect to test DB: %v", err)
}
// Clean database before test
cleanDB(t, db)
// Setup schema
setupSchema(t, db)
return db
}
func cleanDB(t *testing.T, db *mysql.Database) {
tables := []string{"users", "orders", "products"}
for _, table := range tables {
db.Exec("DROP TABLE IF EXISTS " + table)
}
}
func setupSchema(t *testing.T, db *mysql.Database) {
schema := `
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`
if err := db.Exec(schema); err != nil {
t.Fatalf("Failed to create schema: %v", err)
}
}
```
### Using Test Database
```go
func TestInsertUserIntegration(t *testing.T) {
db := testutil.SetupTestDB(t)
user := User{
Name: "Alice",
Email: "alice@example.com",
}
// Insert user
err := db.Insert("users", user)
if err != nil {
t.Fatalf("Insert failed: %v", err)
}
// Verify insertion
var retrieved User
err = db.Select(&retrieved,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
0,
"alice@example.com")
if err != nil {
t.Fatalf("Select failed: %v", err)
}
if retrieved.Name != "Alice" {
t.Errorf("Expected Alice, got %s", retrieved.Name)
}
}
```
## Testing Patterns
### Table-Driven Tests
```go
func TestSelectUsers(t *testing.T) {
tests := []struct {
name string
minAge int
expected []User
expectError bool
}{
{
name: "adults only",
minAge: 18,
expected: []User{
{ID: 1, Name: "Alice", Age: 25},
{ID: 2, Name: "Bob", Age: 30},
},
expectError: false,
},
{
name: "no results",
minAge: 100,
expected: []User{},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
rows := sqlmock.NewRows([]string{"id", "name", "age"})
for _, u := range tt.expected {
rows.AddRow(u.ID, u.Name, u.Age)
}
mock.ExpectQuery("SELECT (.+) FROM `users`").
WithArgs(tt.minAge).
WillReturnRows(rows)
var users []User
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
0,
tt.minAge)
if tt.expectError && err == nil {
t.Error("Expected error, got nil")
}
if !tt.expectError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
if len(users) != len(tt.expected) {
t.Errorf("Expected %d users, got %d",
len(tt.expected), len(users))
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
})
}
}
```
### Testing Named Parameters
```go
func TestNamedParameters(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
// cool-mysql converts @@param to ? internally
mock.ExpectQuery("SELECT (.+) FROM `users` WHERE age > \\? AND `status` = \\?").
WithArgs(18, "active").
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}))
var users []User
err := db.Select(&users,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `status` = @@status",
0,
mysql.Params{"minAge": 18, "status": "active"})
if err != nil {
t.Errorf("Query failed: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
```
### Testing Struct Tags
```go
func TestStructTagMapping(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
type CustomUser struct {
UserID int `mysql:"id"`
Name string `mysql:"user_name"`
}
// Expect query with actual column names
rows := sqlmock.NewRows([]string{"id", "user_name"}).
AddRow(1, "Alice")
mock.ExpectQuery("SELECT `id`, user_name FROM `users`").
WillReturnRows(rows)
var users []CustomUser
err := db.Select(&users, "SELECT `id`, user_name FROM `users`", 0)
if err != nil {
t.Fatalf("Query failed: %v", err)
}
if users[0].UserID != 1 {
t.Errorf("Expected UserID=1, got %d", users[0].UserID)
}
if users[0].Name != "Alice" {
t.Errorf("Expected Name=Alice, got %s", users[0].Name)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
```
## Context-Based Testing
### Testing with Context
```go
func TestSelectWithContext(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "Alice")
mock.ExpectQuery("SELECT (.+) FROM `users`").
WillReturnRows(rows)
var users []User
err := db.SelectContext(ctx, &users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
if err != nil {
t.Errorf("Query failed: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
```
### Testing Context Cancellation
```go
func TestContextCancellation(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
mock.ExpectQuery("SELECT (.+) FROM `users`").
WillDelayFor(100 * time.Millisecond)
var users []User
err := db.SelectContext(ctx, &users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
if err == nil {
t.Error("Expected context cancellation error")
}
}
```
### Testing Database in Context
```go
func TestDatabaseInContext(t *testing.T) {
db := testutil.SetupTestDB(t)
// Store DB in context
ctx := mysql.NewContext(context.Background(), db)
// Retrieve DB from context
retrievedDB := mysql.FromContext(ctx)
if retrievedDB == nil {
t.Error("Expected database in context")
}
// Use DB from context
var users []User
err := retrievedDB.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
if err != nil {
t.Errorf("Query failed: %v", err)
}
}
```
## Testing Caching
### Testing Cache Hits
```go
func TestCacheHit(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
// Enable weak cache for testing
db.UseCache(mysql.NewWeakCache())
rows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "Alice")
// First query - cache miss
mock.ExpectQuery("SELECT (.+) FROM `users`").
WillReturnRows(rows)
var users1 []User
err := db.Select(&users1, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 5*time.Minute)
if err != nil {
t.Fatalf("First query failed: %v", err)
}
// Second query - cache hit (no DB query expected)
var users2 []User
err = db.Select(&users2, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 5*time.Minute)
if err != nil {
t.Fatalf("Second query failed: %v", err)
}
// Verify same results
if len(users1) != len(users2) {
t.Error("Cache returned different results")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}
```
### Testing Cache Bypass
```go
func TestCacheBypass(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
db.UseCache(mysql.NewWeakCache())
rows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "Alice")
// Each query with TTL=0 should hit database
mock.ExpectQuery("SELECT (.+) FROM `users`").
WillReturnRows(rows)
mock.ExpectQuery("SELECT (.+) FROM `users`").
WillReturnRows(rows)
var users []User
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0) // TTL=0, no cache
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0) // TTL=0, no cache
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Expected 2 queries, got different: %v", err)
}
}
```
## Integration Testing
### End-to-End Test
```go
func TestUserWorkflow(t *testing.T) {
db := testutil.SetupTestDB(t)
// Create user
user := User{
Name: "Alice",
Email: "alice@example.com",
}
err := db.Insert("users", user)
if err != nil {
t.Fatalf("Insert failed: %v", err)
}
// Query user
var retrieved User
err = db.Select(&retrieved,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
0,
"alice@example.com")
if err != nil {
t.Fatalf("Select failed: %v", err)
}
// Update user
err = db.Exec("UPDATE `users` SET `name` = @@name WHERE `email` = @@email",
mysql.Params{"name": "Alice Updated", "email": "alice@example.com"})
if err != nil {
t.Fatalf("Update failed: %v", err)
}
// Verify update
err = db.Select(&retrieved,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
0,
"alice@example.com")
if err != nil {
t.Fatalf("Select after update failed: %v", err)
}
if retrieved.Name != "Alice Updated" {
t.Errorf("Expected 'Alice Updated', got '%s'", retrieved.Name)
}
// Delete user
err = db.Exec("DELETE FROM `users` WHERE `email` = @@email",
"alice@example.com")
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
// Verify deletion
err = db.Select(&retrieved,
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
0,
"alice@example.com")
if !errors.Is(err, sql.ErrNoRows) {
t.Error("Expected user to be deleted")
}
}
```
## Best Practices
### 1. Use Helper Functions
```go
func expectUserQuery(mock sqlmock.Sqlmock, users []User) {
rows := sqlmock.NewRows([]string{"id", "name", "email"})
for _, u := range users {
rows.AddRow(u.ID, u.Name, u.Email)
}
mock.ExpectQuery("SELECT (.+) FROM `users`").WillReturnRows(rows)
}
func TestWithHelper(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
expectedUsers := []User{{ID: 1, Name: "Alice", Email: "alice@example.com"}}
expectUserQuery(mock, expectedUsers)
var users []User
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
// Assertions...
}
```
### 2. Test Error Paths
```go
func TestInsertDuplicateEmail(t *testing.T) {
db, mock := setupMockDB(t)
defer mock.ExpectClose()
mock.ExpectExec("INSERT INTO `users`").
WillReturnError(&mysqlDriver.MySQLError{Number: 1062}) // Duplicate entry
user := User{Name: "Alice", Email: "alice@example.com"}
err := db.Insert("users", user)
if err == nil {
t.Error("Expected duplicate key error")
}
}
```
### 3. Clean Up Resources
```go
func TestWithCleanup(t *testing.T) {
db, mock := setupMockDB(t)
t.Cleanup(func() {
mock.ExpectClose()
// Any other cleanup
})
// Test code...
}
```
### 4. Test Concurrent Access
```go
func TestConcurrentAccess(t *testing.T) {
db := testutil.SetupTestDB(t)
var wg sync.WaitGroup
errors := make(chan error, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
user := User{
Name: fmt.Sprintf("User%d", id),
Email: fmt.Sprintf("user%d@example.com", id),
}
if err := db.Insert("users", user); err != nil {
errors <- err
}
}(i)
}
wg.Wait()
close(errors)
for err := range errors {
t.Errorf("Concurrent insert failed: %v", err)
}
// Verify all users inserted
var count int64
count, err := db.Count("SELECT COUNT(*) FROM `users`", 0)
if err != nil {
t.Fatalf("Count failed: %v", err)
}
if count != 10 {
t.Errorf("Expected 10 users, got %d", count)
}
}
```
### 5. Use Subtests
```go
func TestUserOperations(t *testing.T) {
db := testutil.SetupTestDB(t)
t.Run("Insert", func(t *testing.T) {
user := User{Name: "Alice", Email: "alice@example.com"}
err := db.Insert("users", user)
if err != nil {
t.Fatalf("Insert failed: %v", err)
}
})
t.Run("Select", func(t *testing.T) {
var users []User
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
if err != nil {
t.Fatalf("Select failed: %v", err)
}
if len(users) == 0 {
t.Error("Expected at least one user")
}
})
t.Run("Update", func(t *testing.T) {
err := db.Exec("UPDATE `users` SET `name` = @@name WHERE `email` = @@email",
mysql.Params{"name": "Alice Updated", "email": "alice@example.com"})
if err != nil {
t.Fatalf("Update failed: %v", err)
}
})
}
```
### 6. Verify Expectations
```go
func TestAlwaysVerify(t *testing.T) {
db, mock := setupMockDB(t)
defer func() {
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("Unfulfilled expectations: %v", err)
}
}()
// Test code...
}
```
### 7. Test Parameter Interpolation
```go
func TestParameterInterpolation(t *testing.T) {
db, _ := setupMockDB(t)
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `status` = @@status"
params := mysql.Params{"minAge": 18, "status": "active"}
replacedQuery, normalizedParams, err := db.InterpolateParams(query, params)
if err != nil {
t.Fatalf("InterpolateParams failed: %v", err)
}
expectedQuery := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > ? AND `status` = ?"
if replacedQuery != expectedQuery {
t.Errorf("Expected query '%s', got '%s'", expectedQuery, replacedQuery)
}
if len(normalizedParams) != 2 {
t.Errorf("Expected 2 params, got %d", len(normalizedParams))
}
}
```