rqlite

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Dec 2, 2025 License: MIT Imports: 13 Imported by: 1

README

SimpleORM RQLite Implementation

This package provides a direct HTTP-based implementation of the SimpleORM Database interface for RQLite, a lightweight, distributed relational database built on SQLite.

Features

  • Direct HTTP Access: Communicates directly with RQLite's HTTP API endpoints
  • Distributed Database Support: Full support for RQLite cluster operations
  • Consistency Levels: Configurable consistency levels (none, weak, strong)
  • Connection Management: Robust HTTP client with retry logic and timeouts
  • Authentication: Support for basic authentication
  • Cluster Management: Leader/follower awareness and peer discovery
  • Status Monitoring: Comprehensive cluster health monitoring
  • Automatic Batching: Efficient bulk operations with configurable batch sizes

Installation

go get github.com/medatechnology/simpleorm

Quick Start

package main

import (
    "fmt"
    "log"
    
    "github.com/medatechnology/simpleorm/rqlite"
    orm "github.com/medatechnology/simpleorm"
)

func main() {
    // Configure RQLite connection
    config := rqlite.RqliteDirectConfig{
        URL:         "http://localhost:4001",
        Consistency: "strong",
        RetryCount:  3,
    }
    
    // Create database instance
    db, err := rqlite.NewDatabase(config)
    if err != nil {
        log.Fatal("Failed to connect:", err)
    }
    
    // Check connection
    if !db.IsConnected() {
        log.Fatal("Database not connected")
    }
    
    fmt.Println("Successfully connected to RQLite!")
    
    // Get cluster status
    status, err := db.Status()
    if err != nil {
        log.Printf("Could not get status: %v", err)
    } else {
        fmt.Printf("Connected to RQLite %s (Node: %s)\n", status.Version, status.NodeID)
        fmt.Printf("Leader: %t, Nodes: %d\n", status.IsLeader, status.Nodes)
    }
}

Configuration

RqliteDirectConfig Structure
type RqliteDirectConfig struct {
    URL         string        // Base URL for the RQLite node (e.g. "http://localhost:4001")
    Consistency string        // Consistency level: "none", "weak", "strong"
    Username    string        // Optional username for authentication
    Password    string        // Optional password for authentication
    Timeout     time.Duration // HTTP client timeout (default: 60s)
    RetryCount  int           // Number of retries for failed requests (default: 3)
}
Configuration Examples
Minimal Configuration
config := rqlite.RqliteDirectConfig{
    URL: "http://localhost:4001",
}

db, err := rqlite.NewDatabase(config)
Production Configuration
config := rqlite.RqliteDirectConfig{
    URL:         "https://rqlite-cluster.example.com:4001",
    Consistency: "strong",
    Username:    "admin",
    Password:    "secure_password",
    Timeout:     30 * time.Second,
    RetryCount:  5,
}

db, err := rqlite.NewDatabase(config)
Multi-Node Cluster Setup
// Connect to different nodes in a cluster
nodes := []string{
    "http://node1.cluster.com:4001",
    "http://node2.cluster.com:4001", 
    "http://node3.cluster.com:4001",
}

var db *rqlite.RQLiteDirectDB
var err error

// Try connecting to each node until successful
for _, nodeURL := range nodes {
    config := rqlite.RqliteDirectConfig{
        URL:         nodeURL,
        Consistency: "strong",
        Timeout:     10 * time.Second,
        RetryCount:  2,
    }
    
    db, err = rqlite.NewDatabase(config)
    if err == nil && db.IsConnected() {
        fmt.Printf("Connected to %s\n", nodeURL)
        break
    }
    fmt.Printf("Failed to connect to %s: %v\n", nodeURL, err)
}

if err != nil {
    log.Fatal("Failed to connect to any cluster node")
}

Consistency Levels

RQLite supports three consistency levels that balance performance and data consistency:

None (Fastest)
  • Performance: Highest
  • Consistency: No guarantees
  • Use Case: High-volume reads where some staleness is acceptable
config := rqlite.RqliteDirectConfig{
    URL:         "http://localhost:4001",
    Consistency: "none",
}
Weak (Balanced)
  • Performance: Good
  • Consistency: Eventually consistent
  • Use Case: Most application scenarios
config := rqlite.RqliteDirectConfig{
    URL:         "http://localhost:4001",
    Consistency: "weak", // Default if not specified
}
Strong (Most Consistent)
  • Performance: Lower due to consensus
  • Consistency: Linearizable reads
  • Use Case: Critical data requiring immediate consistency
config := rqlite.RqliteDirectConfig{
    URL:         "http://localhost:4001",
    Consistency: "strong",
}

Database Operations

Schema Management
// Get all database schema objects
schemas := db.GetSchema(false, false)
fmt.Printf("Found %d schema objects\n", len(schemas))

// Filter out system tables
schemas = db.GetSchema(true, true) // hideSQL=true, hideSureSQL=true

for _, schema := range schemas {
    fmt.Printf("%-10s %-20s %s\n", schema.ObjectType, schema.TableName, schema.ObjectName)
    
    // Print with SQL details for debugging
    if schema.ObjectType == "table" {
        schema.PrintDebug(true)
    }
}
Table Creation and Management
// Create a users table
createUserTable := `
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    age INTEGER CHECK(age >= 0),
    active BOOLEAN DEFAULT 1,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`

result := db.ExecOneSQL(createUserTable)
if result.Error != nil {
    log.Fatal("Failed to create users table:", result.Error)
}
fmt.Printf("Users table created (time: %s ms)\n", orm.SecondToMsString(result.Timing))

// Create an index
createIndex := "CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)"
result = db.ExecOneSQL(createIndex)
if result.Error != nil {
    log.Printf("Warning: Could not create index: %v", result.Error)
}
Insert Operations
Single Insert with Parameterized Query
// Safe parameterized insert
insertSQL := orm.ParametereizedSQL{
    Query:  "INSERT INTO users (name, email, age, active) VALUES (?, ?, ?, ?)",
    Values: []interface{}{"John Doe", "john@example.com", 30, true},
}

result := db.ExecOneSQLParameterized(insertSQL)
if result.Error != nil {
    log.Fatal("Insert failed:", result.Error)
}

fmt.Printf("User inserted with ID: %d (affected %d rows)\n", 
    result.LastInsertID, result.RowsAffected)
Using DBRecord
// Create a DBRecord
user := orm.DBRecord{
    TableName: "users",
    Data: map[string]interface{}{
        "name":   "Jane Smith",
        "email":  "jane@example.com", 
        "age":    28,
        "active": true,
    },
}

result := db.InsertOneDBRecord(user, false)
if result.Error != nil {
    log.Fatal("DBRecord insert failed:", result.Error)
}
fmt.Printf("DBRecord inserted with ID: %d\n", result.LastInsertID)
Bulk Insert Operations
// Prepare multiple records for bulk insert
users := []orm.DBRecord{
    {
        TableName: "users",
        Data: map[string]interface{}{
            "name":   "Alice Johnson",
            "email":  "alice@example.com",
            "age":    25,
            "active": true,
        },
    },
    {
        TableName: "users", 
        Data: map[string]interface{}{
            "name":   "Bob Wilson",
            "email":  "bob@example.com",
            "age":    32,
            "active": true,
        },
    },
    {
        TableName: "users",
        Data: map[string]interface{}{
            "name":   "Carol Brown",
            "email":  "carol@example.com", 
            "age":    29,
            "active": false,
        },
    },
}

// Efficient bulk insert for same table
results, err := db.InsertManyDBRecordsSameTable(users, false)
if err != nil {
    log.Fatal("Bulk insert failed:", err)
}

// Check results
totalInserted := 0
totalTime := 0.0
for i, result := range results {
    if result.Error != nil {
        fmt.Printf("Record %d failed: %v\n", i, result.Error)
    } else {
        totalInserted += result.RowsAffected
        totalTime += result.Timing
        fmt.Printf("Batch %d: inserted %d records\n", i+1, result.RowsAffected)
    }
}

fmt.Printf("Total: %d records inserted in %s ms\n", 
    totalInserted, orm.SecondToMsString(totalTime))
Query Operations
Basic Queries
// Simple SELECT
allUsers, err := db.SelectMany("users")
if err != nil {
    if err == orm.ErrSQLNoRows {
        fmt.Println("No users found")
    } else {
        log.Fatal("Query failed:", err)
    }
} else {
    fmt.Printf("Found %d users\n", len(allUsers))
    for _, user := range allUsers {
        fmt.Printf("- %s (%s)\n", user.Data["name"], user.Data["email"])
    }
}

// Get single user
user, err := db.SelectOne("users")
if err != nil {
    if err == orm.ErrSQLNoRows {
        fmt.Println("No users in database")
    } else {
        log.Fatal("Query failed:", err)
    }
} else {
    fmt.Printf("First user: %s\n", user.Data["name"])
}
Raw SQL Queries
// Raw SQL with automatic table name detection
sqlQuery := "SELECT name, email, age FROM users WHERE age > 25 ORDER BY age DESC"
records, err := db.SelectOneSQL(sqlQuery)
if err != nil {
    log.Fatal("Raw SQL query failed:", err)
}

fmt.Printf("Users over 25: %d\n", len(records))
for _, record := range records {
    fmt.Printf("- %s: %v years old\n", record.Data["name"], record.Data["age"])
}

// Parameterized raw SQL
paramQuery := orm.ParametereizedSQL{
    Query:  "SELECT * FROM users WHERE age BETWEEN ? AND ? AND active = ? ORDER BY name",
    Values: []interface{}{20, 40, true},
}

records, err = db.SelectOneSQLParameterized(paramQuery)
if err != nil {
    log.Fatal("Parameterized query failed:", err)
}

fmt.Printf("Active users aged 20-40: %d\n", len(records))
Exact Single Record Queries
// SelectOnlyOneSQL ensures exactly one row is returned
findUserSQL := "SELECT * FROM users WHERE email = 'john@example.com'"
user, err := db.SelectOnlyOneSQL(findUserSQL)
if err != nil {
    if err == orm.ErrSQLNoRows {
        fmt.Println("User not found")
    } else if err == orm.ErrSQLMoreThanOneRow {
        fmt.Println("Multiple users found - email should be unique!")
    } else {
        log.Fatal("Query error:", err)
    }
} else {
    fmt.Printf("Found user: %s (ID: %v)\n", user.Data["name"], user.Data["id"])
}
Condition-Based Queries
// Simple condition
condition := &orm.Condition{
    Field:    "age",
    Operator: ">",
    Value:    18,
    OrderBy:  []string{"name ASC"},
    Limit:    10,
}

adults, err := db.SelectManyWithCondition("users", condition)
if err != nil {
    log.Fatal("Condition query failed:", err)
}
fmt.Printf("Found %d adult users\n", len(adults))

// Complex nested conditions
complexCondition := &orm.Condition{
    Logic: "AND",
    Nested: []orm.Condition{
        {
            Logic: "OR", 
            Nested: []orm.Condition{
                {Field: "age", Operator: ">=", Value: 18},
                {Field: "name", Operator: "LIKE", Value: "%admin%"},
            },
        },
        {Field: "active", Operator: "=", Value: true},
    },
    OrderBy: []string{"created_at DESC", "name ASC"},
    Limit:   50,
    Offset:  0,
}

users, err := db.SelectManyWithCondition("users", complexCondition)
if err != nil {
    log.Fatal("Complex condition query failed:", err)
}
fmt.Printf("Found %d users matching complex criteria\n", len(users))

// Generate the SQL to see what was built
sql, values := complexCondition.ToSelectString("users")
fmt.Printf("Generated SQL: %s\n", sql)
fmt.Printf("Parameters: %v\n", values)
Batch Operations
// Multiple different queries in one request
queries := []string{
    "SELECT COUNT(*) as total_users FROM users",
    "SELECT AVG(age) as avg_age FROM users WHERE active = 1",
    "SELECT name, age FROM users ORDER BY age DESC LIMIT 1",
    "SELECT COUNT(*) as inactive_users FROM users WHERE active = 0",
}

results, err := db.SelectManySQL(queries)
if err != nil {
    log.Fatal("Batch queries failed:", err)
}

// Process each query result
for i, records := range results {
    fmt.Printf("Query %d results:\n", i+1)
    for _, record := range records {
        for key, value := range record.Data {
            fmt.Printf("  %s: %v\n", key, value)
        }
    }
    fmt.Println()
}

// Multiple parameterized queries
paramQueries := []orm.ParametereizedSQL{
    {
        Query:  "SELECT COUNT(*) as count FROM users WHERE age > ?",
        Values: []interface{}{30},
    },
    {
        Query:  "SELECT name FROM users WHERE email LIKE ? LIMIT ?",
        Values: []interface{}{"%@example.com", 5},
    },
}

results, err = db.SelectManySQLParameterized(paramQueries)
if err != nil {
    log.Fatal("Batch parameterized queries failed:", err)
}
Update and Delete Operations
// Update with parameterized query
updateSQL := orm.ParametereizedSQL{
    Query:  "UPDATE users SET age = ?, updated_at = CURRENT_TIMESTAMP WHERE email = ?",
    Values: []interface{}{31, "john@example.com"},
}

result := db.ExecOneSQLParameterized(updateSQL)
if result.Error != nil {
    log.Fatal("Update failed:", result.Error)
}
fmt.Printf("Updated %d rows\n", result.RowsAffected)

// Delete with conditions
deleteSQL := orm.ParametereizedSQL{
    Query:  "DELETE FROM users WHERE active = ? AND created_at < ?",
    Values: []interface{}{false, time.Now().AddDate(0, -6, 0)}, // Inactive users older than 6 months
}

result = db.ExecOneSQLParameterized(deleteSQL)
if result.Error != nil {
    log.Fatal("Delete failed:", result.Error)
}
fmt.Printf("Deleted %d inactive users\n", result.RowsAffected)

// Batch updates
updateQueries := []string{
    "UPDATE users SET active = 0 WHERE last_login < date('now', '-1 year')",
    "UPDATE users SET age = age + 1 WHERE birthday = date('now')",
    "DELETE FROM users WHERE active = 0 AND created_at < date('now', '-2 years')",
}

results, err := db.ExecManySQL(updateQueries)
if err != nil {
    log.Fatal("Batch updates failed:", err)
}

for i, result := range results {
    if result.Error != nil {
        fmt.Printf("Update %d failed: %v\n", i+1, result.Error)
    } else {
        fmt.Printf("Update %d: affected %d rows (time: %s ms)\n", 
            i+1, result.RowsAffected, orm.SecondToMsString(result.Timing))
    }
}

Cluster Management and Monitoring

Comprehensive Status Information
// Get detailed cluster status
status, err := db.Status()
if err != nil {
    log.Fatal("Failed to get status:", err)
}

// Print formatted status
fmt.Println("=== RQLite Cluster Status ===")
status.PrintPretty()

// Access individual status fields
fmt.Printf("\nCluster Overview:\n")
fmt.Printf("- DBMS: %s %s\n", status.DBMS, status.Version)
fmt.Printf("- Node ID: %s\n", status.NodeID)
fmt.Printf("- URL: %s\n", status.URL)
fmt.Printf("- Leader: %t\n", status.IsLeader)
fmt.Printf("- Total Nodes: %d\n", status.Nodes)

if status.IsLeader {
    fmt.Printf("- This node is the cluster leader\n")
} else {
    fmt.Printf("- Leader: %s\n", status.Leader)
}

// Database metrics
fmt.Printf("\nDatabase Metrics:\n")
fmt.Printf("- Database Size: %s\n", print.BytesToHumanReadable(status.DBSize, " "))
fmt.Printf("- Directory Size: %s\n", print.BytesToHumanReadable(status.DirSize, " "))
fmt.Printf("- Max Pool Size: %d\n", status.MaxPool)

// Timing information
fmt.Printf("\nTiming Information:\n")
fmt.Printf("- Start Time: %s\n", status.StartTime.Format("2006-01-02 15:04:05"))
fmt.Printf("- Uptime: %s\n", timedate.DurationUptimeShort(status.Uptime))

if !status.LastBackup.IsZero() {
    fmt.Printf("- Last Backup: %s\n", status.LastBackup.Format("2006-01-02 15:04:05"))
}

// Peer information
if len(status.Peers) > 0 {
    fmt.Printf("\nCluster Peers:\n")
    for nodeNum, peer := range status.Peers {
        if peer.NodeNumber != status.NodeNumber {
            fmt.Printf("- Node %d: %s (ID: %s)\n", nodeNum, peer.URL, peer.NodeID)
        }
    }
}
Leader and Peer Discovery
// Check current leader
leader, err := db.Leader()
if err != nil {
    fmt.Printf("Leader information not available: %v\n", err)
} else {
    fmt.Printf("Current cluster leader: %s\n", leader)
}

// Get all cluster peers
peers, err := db.Peers()
if err != nil {
    fmt.Printf("Peer information not available: %v\n", err)
} else {
    fmt.Printf("Cluster has %d peers:\n", len(peers))
    for i, peer := range peers {
        fmt.Printf("  %d. %s\n", i+1, peer)
    }
}

// Health check
if !db.IsConnected() {
    log.Fatal("Database connection is not healthy")
}
fmt.Println("Database connection is healthy")

Error Handling and Resilience

Connection Error Handling
// Test connection with retry
func testConnection(db *rqlite.RQLiteDirectDB) error {
    maxRetries := 5
    for i := 0; i < maxRetries; i++ {
        if db.IsConnected() {
            _, err := db.Status()
            if err == nil {
                return nil
            }
        }
        
        fmt.Printf("Connection attempt %d/%d failed, retrying...\n", i+1, maxRetries)
        time.Sleep(time.Duration(i+1) * time.Second)
    }
    
    return fmt.Errorf("failed to establish connection after %d attempts", maxRetries)
}

if err := testConnection(db); err != nil {
    log.Fatal(err)
}
Query Error Handling
// Handle different types of query errors
func handleQueryError(err error, operation string) {
    if err == nil {
        return
    }
    
    switch err {
    case orm.ErrSQLNoRows:
        fmt.Printf("%s: No data found\n", operation)
    case orm.ErrSQLMoreThanOneRow:
        fmt.Printf("%s: Expected single row, got multiple\n", operation)
    default:
        fmt.Printf("%s failed: %v\n", operation, err)
    }
}

// Example usage
users, err := db.SelectManyWithCondition("users", &orm.Condition{
    Field: "email", Operator: "=", Value: "nonexistent@example.com",
})
handleQueryError(err, "Finding user by email")

user, err := db.SelectOnlyOneSQL("SELECT * FROM users WHERE age = 25")
handleQueryError(err, "Finding single user by age")
Batch Operation Error Handling
// Handle errors in batch operations
queries := []string{
    "SELECT COUNT(*) FROM users",
    "SELECT COUNT(*) FROM nonexistent_table", // This will fail
    "SELECT AVG(age) FROM users",
}

results, err := db.SelectManySQL(queries)
if err != nil {
    fmt.Printf("Batch operation failed: %v\n", err)
} else {
    for i, records := range results {
        if len(records) == 0 {
            fmt.Printf("Query %d returned no results\n", i+1)
        } else {
            fmt.Printf("Query %d succeeded with %d records\n", i+1, len(records))
        }
    }
}

// Check individual operation results
inserts := []orm.DBRecord{
    {TableName: "users", Data: map[string]interface{}{"name": "Test1", "email": "test1@test.com"}},
    {TableName: "invalid_table", Data: map[string]interface{}{"name": "Test2"}}, // This will fail
    {TableName: "users", Data: map[string]interface{}{"name": "Test3", "email": "test3@test.com"}},
}

insertResults, err := db.InsertManyDBRecords(inserts, false)
if err != nil {
    fmt.Printf("Batch insert failed: %v\n", err)
} else {
    successCount := 0
    for i, result := range insertResults {
        if result.Error != nil {
            fmt.Printf("Insert %d failed: %v\n", i+1, result.Error)
        } else {
            successCount++
            fmt.Printf("Insert %d succeeded: ID %d\n", i+1, result.LastInsertID)
        }
    }
    fmt.Printf("Batch complete: %d/%d operations succeeded\n", successCount, len(insertResults))
}

Performance Optimization

Batch Size Configuration
// Check current batch size setting
fmt.Printf("Current max batch size: %d\n", orm.MAX_MULTIPLE_INSERTS)

// Adjust batch size for your workload
// Smaller batches = lower memory usage, more network calls
// Larger batches = higher memory usage, fewer network calls
orm.MAX_MULTIPLE_INSERTS = 50 // Reduce for memory-constrained environments

// Or increase for high-throughput scenarios
orm.MAX_MULTIPLE_INSERTS = 200

// Monitor batch performance
largeDataset := make([]orm.DBRecord, 500)
for i := range largeDataset {
    largeDataset[i] = orm.DBRecord{
        TableName: "users",
        Data: map[string]interface{}{
            "name":  fmt.Sprintf("User%d", i),
            "email": fmt.Sprintf("user%d@test.com", i),
            "age":   20 + (i % 50),
        },
    }
}

startTime := time.Now()
results, err := db.InsertManyDBRecordsSameTable(largeDataset, false)
if err != nil {
    log.Fatal("Large batch insert failed:", err)
}

totalTime := orm.TotalTimeElapsedInSecond(results)
elapsed := time.Since(startTime)

fmt.Printf("Inserted %d records in %d batches\n", len(largeDataset), len(results))
fmt.Printf("Database time: %s ms\n", orm.SecondToMsString(totalTime))
fmt.Printf("Total time: %v\n", elapsed)
fmt.Printf("Throughput: %.2f records/second\n", float64(len(largeDataset))/elapsed.Seconds())
Connection Pool Optimization
// The RQLite implementation automatically configures HTTP connection pooling
// with these optimized settings:

// DEFAULT_MAX_IDLE_CONNECTIONS          = 100
// DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST = 100  
// DEFAULT_MAX_CONNECTIONS_PER_HOST      = 1000
// DEFAULT_IDLE_CONNECTION_TIMEOUT       = 90 * time.Second
// DEFAULT_TIMEOUT                       = 60 * time.Second
// DEFAULT_KEEP_ALIVE                    = 30 * time.Second

// These settings are automatically applied when creating a new database connection
// and are optimized for most production workloads
Query Performance Monitoring
// Monitor query performance
func monitorQuery(db *rqlite.RQLiteDirectDB, description string, queryFunc func() error) {
    start := time.Now()
    err := queryFunc()
    elapsed := time.Since(start)
    
    if err != nil {
        fmt.Printf("❌ %s failed in %v: %v\n", description, elapsed, err)
    } else {
        fmt.Printf("✅ %s completed in %v\n", description, elapsed)
    }
}

// Example usage
monitorQuery(db, "Complex user query", func() error {
    condition := &orm.Condition{
        Logic: "AND",
        Nested: []orm.Condition{
            {Field: "age", Operator: "BETWEEN", Value: []interface{}{20, 40}},
            {Field: "active", Operator: "=", Value: true},
        },
        OrderBy: []string{"created_at DESC"},
        Limit:   100,
    }
    
    _, err := db.SelectManyWithCondition("users", condition)
    return err
})

monitorQuery(db, "Batch status queries", func() error {
    queries := []string{
        "SELECT COUNT(*) FROM users",
        "SELECT COUNT(*) FROM users WHERE active = 1",
        "SELECT AVG(age) FROM users",
    }
    
    _, err := db.SelectManySQL(queries)
    return err
})

RQLite-Specific Features

HTTP Endpoints Used

The implementation leverages these RQLite HTTP API endpoints:

// Read operations
ENDPOINT_QUERY    = "/db/query"    // SELECT statements

// Write operations  
ENDPOINT_EXECUTE  = "/db/execute"  // INSERT, UPDATE, DELETE statements

// Cluster management
ENDPOINT_STATUS   = "/status"      // Cluster status and health
ENDPOINT_READY    = "/readyz"      // Readiness check (future use)

// Backup/Restore (planned features)
ENDPOINT_BACKUP   = "/db/backup"   // Database backup
ENDPOINT_LOAD     = "/db/load"     // Database restore
ENDPOINT_BOOT     = "/boot"        // Database bootstrap
SQLite Data Type Handling

RQLite uses SQLite as its storage engine, so data types are handled according to SQLite's type system:

// SQLite type affinity examples
data := map[string]interface{}{
    // INTEGER affinity
    "id":          1,
    "count":       int64(1000),
    "big_number":  uint64(9223372036854775807),
    
    // TEXT affinity  
    "name":        "John Doe",
    "description": "A long text description",
    "json_data":   `{"key": "value"}`,
    
    // REAL affinity
    "price":       99.99,
    "percentage":  float32(85.5),
    
    // BOOLEAN (stored as INTEGER 0/1)
    "is_active":   true,
    "is_deleted":  false,
    
    // DATETIME (stored as TEXT in ISO8601 format)
    "created_at":  time.Now(),
    "updated_at":  time.Date(2023, 12, 25, 10, 30, 0, 0, time.UTC),
    
    // BLOB affinity
    "binary_data": []byte("binary content"),
    "image":       []byte{0x89, 0x50, 0x4E, 0x47}, // PNG header
}

record := orm.DBRecord{
    TableName: "mixed_types",
    Data:      data,
}

result := db.InsertOneDBRecord(record, false)
if result.Error != nil {
    log.Fatal("Failed to insert mixed types:", result.Error)
}
Advanced SQL Features
// Common Table Expressions (CTEs)
cteQuery := `
WITH RECURSIVE user_hierarchy AS (
    SELECT id, name, manager_id, 1 as level
    FROM users 
    WHERE manager_id IS NULL
    
    UNION ALL
    
    SELECT u.id, u.name, u.manager_id, uh.level + 1
    FROM users u
    JOIN user_hierarchy uh ON u.manager_id = uh.id
)
SELECT * FROM user_hierarchy ORDER BY level, name`

records, err := db.SelectOneSQL(cteQuery)
if err != nil {
    log.Fatal("CTE query failed:", err)
}

// Window functions
windowQuery := `
SELECT 
    name,
    age,
    department,
    salary,
    AVG(salary) OVER (PARTITION BY department) as dept_avg_salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as dept_rank
FROM employees
ORDER BY department, dept_rank`

records, err = db.SelectOneSQL(windowQuery)

// JSON operations (SQLite 3.38+)
jsonQuery := orm.ParametereizedSQL{
    Query: `
    SELECT 
        id,
        name,
        JSON_EXTRACT(metadata, '$.department') as department,
        JSON_EXTRACT(metadata, '$.skills') as skills
    FROM users 

Documentation

Index

Constants

View Source
const (
	// Primary result codes
	ErrCodeSQLiteOK         = 0  // Successful result
	ErrCodeSQLiteError      = 1  // Generic error
	ErrCodeSQLiteBusy       = 5  // Database is locked
	ErrCodeSQLiteReadonly   = 8  // Attempt to write a readonly database
	ErrCodeSQLiteConstraint = 19 // Constraint violation
	ErrCodeSQLiteNotADB     = 26 // File opened that is not a database

	// Extended result codes for constraint violations
	ErrCodeSQLiteConstraintUnique     = 2067 // UNIQUE constraint failed
	ErrCodeSQLiteConstraintPrimaryKey = 1555 // PRIMARY KEY constraint failed
	ErrCodeSQLiteConstraintNotNull    = 1299 // NOT NULL constraint failed
	ErrCodeSQLiteConstraintForeignKey = 787  // FOREIGN KEY constraint failed
	ErrCodeSQLiteConstraintCheck      = 275  // CHECK constraint failed

	// HTTP status codes for RQLite-specific errors
	HTTPStatusUnauthorized       = http.StatusUnauthorized        // 401
	HTTPStatusForbidden          = http.StatusForbidden           // 403
	HTTPStatusNotFound           = http.StatusNotFound            // 404
	HTTPStatusBadRequest         = http.StatusBadRequest          // 400
	HTTPStatusInternalError      = http.StatusInternalServerError // 500
	HTTPStatusServiceUnavailable = http.StatusServiceUnavailable  // 503
)

SQLite error codes (RQLite uses SQLite under the hood) See: https://www.sqlite.org/rescode.html

View Source
const (
	ErrMsgUniqueConstraint     = "UNIQUE constraint failed"
	ErrMsgPrimaryKeyConstraint = "PRIMARY KEY constraint failed"
	ErrMsgNotNullConstraint    = "NOT NULL constraint failed"
	ErrMsgForeignKeyConstraint = "FOREIGN KEY constraint failed"
	ErrMsgCheckConstraint      = "CHECK constraint failed"
	ErrMsgDatabaseLocked       = "database is locked"
	ErrMsgReadonlyDatabase     = "attempt to write a readonly database"
	ErrMsgNoSuchTable          = "no such table"
	ErrMsgNoSuchColumn         = "no such column"
	ErrMsgSyntaxError          = "syntax error"
)

Common error messages from SQLite/RQLite

View Source
const (
	PREFIX_SURESQL_TABLE = "_"
	PREFIX_SQLITE_TABLE  = "sqlite_"
	SCHEMA_TABLE         = "sqlite_master"

	// Default HTTP timeouts
	// TODO: maybe read this from env or pass it from Config?
	DEFAULT_CONNECTION_TIMEOUT            = 60 * time.Second
	DEFAULT_TIMEOUT                       = 60 * time.Second
	DEFAULT_DIAL_TIMEOUT                  = 60 * time.Second
	DEFAULT_KEEP_ALIVE                    = 30 * time.Second
	DEFAULT_TLS_HANDSHAKE_TIMEOUT         = 30 * time.Second
	DEFAULT_RESPONSE_TIMEOUT              = 60 * time.Second
	DEFAULT_CONTINUE_TIMEOUT              = 5 * time.Second
	DEFAULT_MAX_IDLE_CONNECTIONS          = 100
	DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST = 100
	DEFAULT_MAX_CONNECTIONS_PER_HOST      = 1000
	DEFAULT_IDLE_CONNECTION_TIMEOUT       = 90 * time.Second
	DEFAULT_RETRY_TIMEOUT                 = 2 * time.Second
	DEFAULT_MAX_RETRIES                   = 3

	// RQLite API endpoints
	ENDPOINT_EXECUTE       = "/db/execute"
	ENDPOINT_QUERY         = "/db/query"
	ENDPOINT_UNIFIED       = "/db/request"
	ENDPOINT_LOAD          = "/db/load"
	ENDPOINT_BACKUP        = "/db/backup" // curl -s -XGET localhost:4001/db/backup -o bak.sqlite3 (returns might need to be saved as a file right away)
	ENDPOINT_BOOT          = "/boot"
	ENDPOINT_SNAPSHOT      = "/snapshot"
	ENDPOINT_STATUS        = "/status" // option are /status?pretty
	ENDPOINT_READY         = "/readyz" // option readyz?noleader or /readyz?sync&timeout=5s
	ENDPOINT_JOIN          = "/join"
	ENDPOINT_REMOVE        = "/remove"
	ENDPOINT_NODE          = "/nodes" // can have nodes?pretty&ver=2 for improved JSON format
	ENDPOINT_DEBUG         = "/debug"
	ENDPOINT_VARS          = "/debug/vars"
	ENDPOINT_PPROF_CMDLINE = "/debug/pprof/cmdline"
	ENDPOINT_PPROF_PROFILE = "/debug/pprof/profile"
	ENDPOINT_PPROF_SYMBOL  = "/debug/pprof/symbol"

	DEFAULT_MAX_POOL = 25
)

Variables

View Source
var (
	ErrRQLiteNotConnected       medaerror.MedaError = medaerror.MedaError{Message: "RQLite database is not connected"}
	ErrRQLiteInvalidURL         medaerror.MedaError = medaerror.MedaError{Message: "invalid RQLite URL"}
	ErrRQLiteConnectionFailed   medaerror.MedaError = medaerror.MedaError{Message: "failed to connect to RQLite server"}
	ErrRQLiteQueryFailed        medaerror.MedaError = medaerror.MedaError{Message: "RQLite query execution failed"}
	ErrRQLiteExecuteFailed      medaerror.MedaError = medaerror.MedaError{Message: "RQLite execute command failed"}
	ErrRQLiteInvalidConfig      medaerror.MedaError = medaerror.MedaError{Message: "invalid RQLite configuration"}
	ErrRQLiteTimeout            medaerror.MedaError = medaerror.MedaError{Message: "RQLite operation timed out"}
	ErrRQLiteUnauthorized       medaerror.MedaError = medaerror.MedaError{Message: "RQLite authentication failed"}
	ErrRQLiteNodeUnavailable    medaerror.MedaError = medaerror.MedaError{Message: "RQLite node is unavailable"}
	ErrRQLiteReadonly           medaerror.MedaError = medaerror.MedaError{Message: "RQLite node is in readonly mode"}
	ErrRQLiteDatabaseLocked     medaerror.MedaError = medaerror.MedaError{Message: "RQLite database is locked"}
	ErrRQLiteInvalidJSON        medaerror.MedaError = medaerror.MedaError{Message: "invalid JSON response from RQLite"}
	ErrRQLiteConsistencyFailure medaerror.MedaError = medaerror.MedaError{Message: "RQLite consistency requirement not met"}
)

Custom RQLite errors using medaerror

Functions

func ConvertRQLiteError added in v0.0.4

func ConvertRQLiteError(err error) error

ConvertRQLiteError converts an RQLite error to an appropriate ORM error

func FormatRQLiteError added in v0.0.4

func FormatRQLiteError(err error) string

FormatRQLiteError formats an RQLite error for logging or display

func GetRQLiteErrorDetails added in v0.0.4

func GetRQLiteErrorDetails(err error) (operation, table, query, message, detail string, statusCode int)

GetRQLiteErrorDetails extracts detailed error information

func GetStatusInfoFromResponse

func GetStatusInfoFromResponse(raw map[string]interface{}) (orm.NodeStatusStruct, error)

When calling rqlite/status it returns long JSON format we only take what is needed

func IsAuthenticationError added in v0.0.4

func IsAuthenticationError(err error) bool

IsAuthenticationError checks if the error is an authentication failure

func IsCheckViolation added in v0.0.4

func IsCheckViolation(err error) bool

IsCheckViolation checks if the error is a CHECK constraint violation

func IsColumnNotFound added in v0.0.4

func IsColumnNotFound(err error) bool

IsColumnNotFound checks if the error is due to a non-existent column

func IsConnectionError added in v0.0.4

func IsConnectionError(err error) bool

IsConnectionError checks if the error is related to connection failure

func IsConstraintViolation added in v0.0.4

func IsConstraintViolation(err error) bool

IsConstraintViolation checks if the error is any type of constraint violation

func IsDatabaseLocked added in v0.0.4

func IsDatabaseLocked(err error) bool

IsDatabaseLocked checks if the error is due to database being locked

func IsForeignKeyViolation added in v0.0.4

func IsForeignKeyViolation(err error) bool

IsForeignKeyViolation checks if the error is a FOREIGN KEY constraint violation

func IsHTTPError added in v0.0.4

func IsHTTPError(err error, statusCode int) bool

IsHTTPError checks if the error is an HTTP error with a specific status code

func IsNodeUnavailable added in v0.0.4

func IsNodeUnavailable(err error) bool

IsNodeUnavailable checks if the error is due to node being unavailable

func IsNotNullViolation added in v0.0.4

func IsNotNullViolation(err error) bool

IsNotNullViolation checks if the error is a NOT NULL constraint violation

func IsPrimaryKeyViolation added in v0.0.4

func IsPrimaryKeyViolation(err error) bool

IsPrimaryKeyViolation checks if the error is a PRIMARY KEY constraint violation

func IsReadonlyError added in v0.0.4

func IsReadonlyError(err error) bool

IsReadonlyError checks if the error is due to attempting to write to a readonly database

func IsRetryable added in v0.0.4

func IsRetryable(err error) bool

IsRetryable checks if the error is transient and the operation can be retried

func IsSyntaxError added in v0.0.4

func IsSyntaxError(err error) bool

IsSyntaxError checks if the error is a SQL syntax error

func IsTableNotFound added in v0.0.4

func IsTableNotFound(err error) bool

IsTableNotFound checks if the error is due to a non-existent table

func IsUniqueViolation added in v0.0.4

func IsUniqueViolation(err error) bool

IsUniqueViolation checks if the error is a UNIQUE constraint violation

func WrapRQLiteError added in v0.0.4

func WrapRQLiteError(err error, operation, table, query string) error

WrapRQLiteError wraps an error with RQLite-specific context

func WrapRQLiteHTTPError added in v0.0.4

func WrapRQLiteHTTPError(err error, operation, table, query string, statusCode int) error

WrapRQLiteHTTPError wraps an HTTP error with status code

Types

type ExecuteResponse

type ExecuteResponse struct {
	Results []ExecuteResult `json:"results"`
	Time    float64         `json:"time"`
}

ExecuteResponse represents the response from a write operation

type ExecuteResult

type ExecuteResult struct {
	LastInsertID int     `json:"last_insert_id"`
	RowsAffected int     `json:"rows_affected"`
	Time         float64 `json:"time"`
	Error        string  `json:"error,omitempty"`
}

ExecuteResult represents a single result from a write operation

type QueryResponse

type QueryResponse struct {
	Results []QueryResult `json:"results"`
	Time    float64       `json:"time"`
}

QueryResponse represents the response from a read operation

type QueryResult

type QueryResult struct {
	Columns []string        `json:"columns"`
	Types   []string        `json:"types"`
	Values  [][]interface{} `json:"values"`
	Time    float64         `json:"time"`
	Error   string          `json:"error,omitempty"`
}

QueryResult represents a single result from a read operation

type RQLiteDirectDB

type RQLiteDirectDB struct {
	Config     RqliteDirectConfig
	HTTPClient *http.Client
}

RQLiteDirectDB implements the orm.Database interface for direct HTTP access to RQLite

func NewDatabase

func NewDatabase(config RqliteDirectConfig) (*RQLiteDirectDB, error)

NewDatabase creates a new RQLiteDirectDB instance

func (*RQLiteDirectDB) BeginTransaction added in v0.2.0

func (db *RQLiteDirectDB) BeginTransaction() (orm.Transaction, error)

BeginTransaction starts a new transaction by creating a transaction buffer

func (*RQLiteDirectDB) ExecManySQL

func (db *RQLiteDirectDB) ExecManySQL(sqls []string) ([]orm.BasicSQLResult, error)

ExecManySQL executes multiple SQL statements

func (*RQLiteDirectDB) ExecManySQLParameterized

func (db *RQLiteDirectDB) ExecManySQLParameterized(paramSQLs []orm.ParametereizedSQL) ([]orm.BasicSQLResult, error)

ExecManySQLParameterized executes multiple parameterized SQL statements

func (*RQLiteDirectDB) ExecOneSQL

func (db *RQLiteDirectDB) ExecOneSQL(sql string) orm.BasicSQLResult

ExecOneSQL executes a single SQL statement

func (*RQLiteDirectDB) ExecOneSQLParameterized

func (db *RQLiteDirectDB) ExecOneSQLParameterized(paramSQL orm.ParametereizedSQL) orm.BasicSQLResult

ExecOneSQLParameterized executes a single parameterized SQL statement

func (*RQLiteDirectDB) GetSchema

func (db *RQLiteDirectDB) GetSchema(hideSQL, hideSureSQL bool) []orm.SchemaStruct

GetSchema returns the database schema

func (*RQLiteDirectDB) InsertManyDBRecords

func (db *RQLiteDirectDB) InsertManyDBRecords(records []orm.DBRecord, queue bool) ([]orm.BasicSQLResult, error)

InsertManyDBRecords inserts multiple records

func (*RQLiteDirectDB) InsertManyDBRecordsSameTable

func (db *RQLiteDirectDB) InsertManyDBRecordsSameTable(records []orm.DBRecord, queue bool) ([]orm.BasicSQLResult, error)

InsertManyDBRecordsSameTable inserts multiple records into the same table

func (*RQLiteDirectDB) InsertManyTableStructs

func (db *RQLiteDirectDB) InsertManyTableStructs(objs []orm.TableStruct, queue bool) ([]orm.BasicSQLResult, error)

InsertManyTableStructs inserts multiple table structs

func (*RQLiteDirectDB) InsertOneDBRecord

func (db *RQLiteDirectDB) InsertOneDBRecord(record orm.DBRecord, queue bool) orm.BasicSQLResult

InsertOneDBRecord inserts a single record

func (*RQLiteDirectDB) InsertOneTableStruct

func (db *RQLiteDirectDB) InsertOneTableStruct(obj orm.TableStruct, queue bool) orm.BasicSQLResult

InsertOneTableStruct inserts a single table struct

func (*RQLiteDirectDB) IsConnected

func (db *RQLiteDirectDB) IsConnected() bool

IsConnected checks if the database connection is alive

func (*RQLiteDirectDB) Leader

func (db *RQLiteDirectDB) Leader() (string, error)

Leader returns the leader node of the RQLite cluster

func (*RQLiteDirectDB) Peers

func (db *RQLiteDirectDB) Peers() ([]string, error)

Peers returns the peer nodes of the RQLite cluster

func (*RQLiteDirectDB) SelectMany

func (db *RQLiteDirectDB) SelectMany(tableName string) (orm.DBRecords, error)

SelectMany selects multiple records from the table

func (*RQLiteDirectDB) SelectManyComplex added in v0.1.0

func (db *RQLiteDirectDB) SelectManyComplex(query *orm.ComplexQuery) ([]orm.DBRecord, error)

SelectManyComplex executes a complex query with JOINs, custom SELECT fields, GROUP BY, etc. It supports advanced SQL features through the ComplexQuery struct.

Example:

query := &orm.ComplexQuery{
    Select: []string{"users.id", "users.name", "COUNT(orders.id) as order_count"},
    From:   "users",
    Joins: []orm.Join{
        {Type: orm.LeftJoin, Table: "orders", Condition: "users.id = orders.user_id"},
    },
    Where: &orm.Condition{Field: "users.status", Operator: "=", Value: "active"},
    GroupBy: []string{"users.id", "users.name"},
    Having: "COUNT(orders.id) > 5",
    OrderBy: []string{"order_count DESC"},
    Limit: 10,
}
records, err := db.SelectManyComplex(query)

func (*RQLiteDirectDB) SelectManySQL

func (db *RQLiteDirectDB) SelectManySQL(sqls []string) ([]orm.DBRecords, error)

SelectManySQL executes multiple SQL queries and returns the results of each

func (*RQLiteDirectDB) SelectManySQLParameterized

func (db *RQLiteDirectDB) SelectManySQLParameterized(paramSQLs []orm.ParametereizedSQL) ([]orm.DBRecords, error)

SelectManySQLParameterized executes multiple parameterized SQL queries

func (*RQLiteDirectDB) SelectManyWithCondition

func (db *RQLiteDirectDB) SelectManyWithCondition(tableName string, condition *orm.Condition) ([]orm.DBRecord, error)

SelectManyWithCondition selects multiple records with a condition

func (*RQLiteDirectDB) SelectOne

func (db *RQLiteDirectDB) SelectOne(tableName string) (orm.DBRecord, error)

SelectOne selects a single record from the table

func (*RQLiteDirectDB) SelectOneComplex added in v0.1.0

func (db *RQLiteDirectDB) SelectOneComplex(query *orm.ComplexQuery) (orm.DBRecord, error)

SelectOneComplex executes a complex query and ensures exactly one row is returned. It's similar to SelectManyComplex but validates that only one record is returned.

Example:

query := &orm.ComplexQuery{
    Select: []string{"users.*", "profiles.bio"},
    From:   "users",
    Joins: []orm.Join{
        {Type: orm.InnerJoin, Table: "profiles", Condition: "users.id = profiles.user_id"},
    },
    Where: &orm.Condition{Field: "users.id", Operator: "=", Value: 123},
}
record, err := db.SelectOneComplex(query)

func (*RQLiteDirectDB) SelectOneSQL

func (db *RQLiteDirectDB) SelectOneSQL(sql string) (orm.DBRecords, error)

SelectOneSQL executes a single SQL query and returns the results

func (*RQLiteDirectDB) SelectOneSQLParameterized

func (db *RQLiteDirectDB) SelectOneSQLParameterized(paramSQL orm.ParametereizedSQL) (orm.DBRecords, error)

SelectOneSQLParameterized executes a single parameterized SQL query

func (*RQLiteDirectDB) SelectOneWithCondition

func (db *RQLiteDirectDB) SelectOneWithCondition(tableName string, condition *orm.Condition) (orm.DBRecord, error)

SelectOneWithCondition selects a single record with a condition

func (*RQLiteDirectDB) SelectOnlyOneSQL

func (db *RQLiteDirectDB) SelectOnlyOneSQL(sql string) (orm.DBRecord, error)

SelectOnlyOneSQL executes a SQL query and ensures exactly one row is returned

func (*RQLiteDirectDB) SelectOnlyOneSQLParameterized

func (db *RQLiteDirectDB) SelectOnlyOneSQLParameterized(paramSQL orm.ParametereizedSQL) (orm.DBRecord, error)

SelectOnlyOneSQLParameterized executes a parameterized SQL query and ensures exactly one row is returned

func (*RQLiteDirectDB) Status

func (db *RQLiteDirectDB) Status() (orm.NodeStatusStruct, error)

Status returns the status of the RQLite cluster

type RQLiteError added in v0.0.4

type RQLiteError struct {
	Operation  string // The operation that failed (e.g., "SELECT", "INSERT", "EXECUTE")
	Table      string // The table involved (if applicable)
	Query      string // The SQL query that failed (if applicable)
	StatusCode int    // HTTP status code (if applicable)
	Message    string // Error message
	Detail     string // Detailed error information from RQLite
	Err        error  // Original error
}

RQLiteError wraps RQLite-specific errors with additional context

func (*RQLiteError) Error added in v0.0.4

func (e *RQLiteError) Error() string

Error implements the error interface

func (*RQLiteError) Unwrap added in v0.0.4

func (e *RQLiteError) Unwrap() error

Unwrap returns the underlying error

type RqliteDirectConfig

type RqliteDirectConfig struct {
	URL         string        // Base URL for the RQLite node (e.g. "http://localhost:4001")
	Consistency string        // Consistency level: "none", "weak", "strong"
	Username    string        // Optional username for authentication
	Password    string        // Optional password for authentication
	Timeout     time.Duration // HTTP client timeout
	RetryCount  int           // Number of retries for failed requests
}

RqliteDirectConfig holds configuration for direct RQLite connections

Jump to

Keyboard shortcuts

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