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