Skip to main content

Overview

ChameleonDB includes multiple layers of safety guards to prevent accidental data loss or corruption. These protections are built into the mutation system and cannot be disabled without explicit confirmation.

WHERE Clause Requirement

The most important safety guard: all updates and deletes require a WHERE clause.

The Problem

In traditional databases, forgetting a WHERE clause can be catastrophic:
-- Intended to update one user
UPDATE users SET role = 'admin' WHERE id = '...';

-- Accidentally ran this instead (missing WHERE)
UPDATE users SET role = 'admin';
-- ❌ ALL users are now admins!

ChameleonDB’s Solution

ChameleonDB prevents this at the API level:
// This will FAIL with SafetyError
db.Update("User").
    Set("role", "admin").
    Execute(ctx)

// Error: SafetyError: UPDATE requires a WHERE clause
//        Suggestion: Use Filter() or ForceUpdateAll()
The WHERE clause requirement applies to both Update and Delete operations. This is enforced before any SQL is generated.

Safe Approach

Always use .Filter() to specify which records to modify:
// Good: Explicit filter
db.Update("User").
    Filter("id", "eq", userID).
    Set("role", "admin").
    Execute(ctx)

// Good: Multiple filters for bulk operations
db.Update("Post").
    Filter("author_id", "eq", authorID).
    Filter("published", "eq", false).
    Set("status", "draft").
    Execute(ctx)

Intentional Full-Table Operations

If you genuinely need to update/delete all records, use the explicit bypass:
// Update all records (requires explicit confirmation)
db.Update("User").
    Set("notification_enabled", true).
    ForceUpdateAll(). // Explicit bypass
    Execute(ctx)

// Delete all records (requires explicit confirmation)
db.Delete("TempSession").
    ForceDeleteAll(). // Explicit bypass
    Execute(ctx)
Use with extreme caution: ForceUpdateAll() and ForceDeleteAll() bypass the safety guard and affect every record in the table.Recommended safeguards:
  • Only use in development/test environments
  • Require code review before production use
  • Always have recent backups
  • Test on a copy of production data first
  • Document why the full-table operation is necessary

Three-Stage Validation Pipeline

Every mutation goes through three validation stages before execution:

Stage 1: Schema Validation

Verifies the operation against your schema definition:
// Schema validation catches:
// - Non-existent entities
// - Non-existent fields
// - Type mismatches
// - Invalid field references

db.Insert("User").
    Set("id", uuid.New().String()).
    Set("email", 12345). // Wrong type!
    Execute(ctx)

// Error: Type mismatch: field 'email' expects string, got int
What it checks:
  • Entity exists in schema
  • All fields are defined
  • Field types match schema
  • Required fields are present

Stage 2: Constraint Validation

Enforces logical constraints before hitting the database:
// Constraint validation catches:
// - Missing required fields
// - NULL values in NOT NULL fields
// - Invalid filter operators
// - Type coercion issues

db.Insert("User").
    Set("id", uuid.New().String()).
    Set("email", "ana@mail.com").
    // Missing required 'name' field
    Execute(ctx)

// Error: NotNullError: Field 'name' cannot be null
//        Schema requires this field
//        Suggestion: Provide a value for 'name'
What it checks:
  • NOT NULL constraints
  • Required field presence
  • WHERE clause requirement (for updates/deletes)
  • Valid filter operators

Stage 3: Database Validation

Final enforcement at the database level:
// Database validation catches:
// - UNIQUE constraint violations
// - Foreign key violations
// - Database-specific constraints
// - Check constraints

db.Insert("User").
    Set("id", uuid.New().String()).
    Set("email", "ana@mail.com"). // Email already exists
    Set("name", "Ana").
    Execute(ctx)

// Error: UniqueConstraintError: Field 'email' must be unique
//        Value: ana@mail.com already exists
//        Suggestion: Use a different value or update the existing record
What it checks:
  • UNIQUE constraints
  • Foreign key references
  • Check constraints
  • Database triggers

Error Hierarchy

ChameleonDB provides typed errors for precise error handling:
import "errors"

result, err := db.Insert("User").
    Set("id", uuid.New().String()).
    Set("email", "ana@mail.com").
    Set("name", "Ana").
    Execute(ctx)

if err != nil {
    // Check specific error types
    var uniqueErr *engine.UniqueConstraintError
    if errors.As(err, &uniqueErr) {
        // Handle duplicate entry
        log.Printf("Duplicate email: %s", uniqueErr.Value)
        return fmt.Errorf("email already registered")
    }
    
    var fkErr *engine.ForeignKeyError
    if errors.As(err, &fkErr) {
        // Handle invalid reference
        log.Printf("Invalid FK: %s", fkErr.Field)
        return fmt.Errorf("invalid reference")
    }
    
    var notNullErr *engine.NotNullError
    if errors.As(err, &notNullErr) {
        // Handle missing required field
        log.Printf("Missing field: %s", notNullErr.Field)
        return fmt.Errorf("missing required field")
    }
    
    var safetyErr *engine.SafetyError
    if errors.As(err, &safetyErr) {
        // Handle missing WHERE clause
        log.Printf("Safety violation: %s", safetyErr.Message)
        return fmt.Errorf("operation requires filter")
    }
    
    // Unknown error
    return fmt.Errorf("operation failed: %w", err)
}

Error Types

Error TypeWhen It OccursHTTP Status
SafetyErrorMissing WHERE clause in update/delete400 Bad Request
UniqueConstraintErrorDuplicate value in unique field409 Conflict
ForeignKeyErrorInvalid reference to parent record400 Bad Request
NotNullErrorNULL value in required field400 Bad Request
TypeMismatchErrorWrong data type for field400 Bad Request
EntityNotFoundErrorUnknown entity in schema400 Bad Request
FieldNotFoundErrorUnknown field in entity400 Bad Request

HTTP Error Mapping

Recommended pattern for web applications:
import "net/http"

func mapMutationError(err error) (status int, message string) {
    var uniqueErr *engine.UniqueConstraintError
    if errors.As(err, &uniqueErr) {
        return http.StatusConflict, uniqueErr.Error()
    }
    
    var fkErr *engine.ForeignKeyError
    if errors.As(err, &fkErr) {
        return http.StatusBadRequest, fkErr.Error()
    }
    
    var notNullErr *engine.NotNullError
    if errors.As(err, &notNullErr) {
        return http.StatusBadRequest, notNullErr.Error()
    }
    
    var safetyErr *engine.SafetyError
    if errors.As(err, &safetyErr) {
        return http.StatusBadRequest, safetyErr.Error()
    }
    
    // Generic error
    return http.StatusInternalServerError, "Internal server error"
}

// Usage in handler
func createUserHandler(w http.ResponseWriter, r *http.Request) {
    result, err := repo.Create(r.Context(), email, name)
    if err != nil {
        status, message := mapMutationError(err)
        http.Error(w, message, status)
        return
    }
    
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(result)
}

Validation Best Practices

1. Validate Early

Validate input before attempting mutations:
func (r *UserRepository) Create(ctx context.Context, email, name string) error {
    // Validate email format
    if !isValidEmail(email) {
        return fmt.Errorf("invalid email format")
    }
    
    // Validate name length
    if len(name) < 2 {
        return fmt.Errorf("name too short (minimum 2 characters)")
    }
    
    if len(name) > 100 {
        return fmt.Errorf("name too long (maximum 100 characters)")
    }
    
    // Perform insert
    _, err := r.eng.Insert("User").
        Set("id", uuid.New().String()).
        Set("email", email).
        Set("name", name).
        Execute(ctx)
    
    return err
}

2. Use Typed Errors

Provide meaningful error messages to users:
func (r *UserRepository) Create(ctx context.Context, email, name string) error {
    result, err := r.eng.Insert("User").
        Set("id", uuid.New().String()).
        Set("email", email).
        Set("name", name).
        Execute(ctx)
    
    if err != nil {
        var uniqueErr *engine.UniqueConstraintError
        if errors.As(err, &uniqueErr) {
            if uniqueErr.Field == "email" {
                return fmt.Errorf("email %s is already registered", email)
            }
        }
        return fmt.Errorf("failed to create user: %w", err)
    }
    
    return nil
}

3. Check Affected Count

Verify operations affected the expected number of records:
func (r *UserRepository) UpdateEmail(ctx context.Context, id, email string) error {
    result, err := r.eng.Update("User").
        Filter("id", "eq", id).
        Set("email", email).
        Execute(ctx)
    
    if err != nil {
        return err
    }
    
    // Verify exactly one record was updated
    if result.Affected == 0 {
        return fmt.Errorf("user not found: %s", id)
    }
    
    if result.Affected > 1 {
        log.Printf("Warning: Updated %d users (expected 1)", result.Affected)
    }
    
    return nil
}

4. Use Debug Mode During Development

Enable debug mode to see generated SQL:
result, err := eng.Update("User").
    Filter("id", "eq", userID).
    Set("name", "Ana").
    Debug(). // Shows SQL and arguments
    Execute(ctx)

// Output:
// [SQL] Update User
// UPDATE users SET name = $1 WHERE id = $2
// [ARGS] [Ana, 550e8400-e29b-41d4-a716-446655440000]

Pre-Flight Checklist

Before running mutations, verify:

For Development

  • Engine created with engine.NewEngine()
  • Mutation package imported: _ "github.com/chameleon-db/chameleondb/chameleon/pkg/engine/mutation"
  • Connected to database with eng.Connect(ctx, cfg)
  • Database connection verified with eng.Ping(ctx)
  • Schema loaded successfully
  • Debug mode enabled for diagnostics

For Updates/Deletes

  • .Filter() clause is present
  • Filter targets correct records
  • Expected affected count is known
  • Tested with .Debug() first
  • Foreign key relationships understood

For Production

  • Input validation implemented
  • Error handling covers all typed errors
  • HTTP status codes mapped correctly
  • User-friendly error messages provided
  • Logging includes context (user ID, operation, etc.)
  • Backups are current
  • Rollback plan exists

Common Safety Violations

Missing WHERE Clause

// ❌ WRONG: No filter
db.Update("User").Set("active", true).Execute(ctx)

// ✅ CORRECT: Specific filter
db.Update("User").
    Filter("id", "eq", userID).
    Set("active", true).
    Execute(ctx)

// ✅ CORRECT: Intentional full-table update
db.Update("User").
    Set("migrated", true).
    ForceUpdateAll().
    Execute(ctx)

Type Mismatches

// ❌ WRONG: Wrong type
db.Insert("User").
    Set("email", 12345). // email is string, not int
    Execute(ctx)

// ✅ CORRECT: Correct type
db.Insert("User").
    Set("email", "user@mail.com").
    Execute(ctx)

Missing Required Fields

// ❌ WRONG: Missing required 'name'
db.Insert("User").
    Set("id", uuid.New().String()).
    Set("email", "user@mail.com").
    Execute(ctx)

// ✅ CORRECT: All required fields
db.Insert("User").
    Set("id", uuid.New().String()).
    Set("email", "user@mail.com").
    Set("name", "User Name").
    Execute(ctx)

Invalid Foreign Keys

// ❌ WRONG: Author doesn't exist
db.Insert("Post").
    Set("id", uuid.New().String()).
    Set("title", "Post").
    Set("author_id", "invalid-uuid").
    Execute(ctx)

// ✅ CORRECT: Valid author reference
db.Insert("Post").
    Set("id", uuid.New().String()).
    Set("title", "Post").
    Set("author_id", existingUserID).
    Execute(ctx)

Next Steps