README
¶
Database Driver API
This document covers the low-level database driver API for advanced usage and direct database operations. For the high-level ORM API, see the main README.
Driver Architecture
RediORM uses a three-layer driver architecture:
- URI Layer - Database connection URIs (e.g.,
sqlite://./app.db) - Native DSN Layer - Database-specific connection strings
- Connection Layer - Actual database connections
Supported Drivers
- SQLite - File-based database with in-memory support
- MySQL - Full-featured MySQL/MariaDB support
- PostgreSQL - Complete PostgreSQL support
- MongoDB - Document database with SQL query translation
Direct Driver Usage
Importing Drivers
Import only the drivers you need:
import (
"github.com/rediwo/redi-orm/database"
_ "github.com/rediwo/redi-orm/drivers/sqlite" // SQLite driver
_ "github.com/rediwo/redi-orm/drivers/mysql" // MySQL driver
_ "github.com/rediwo/redi-orm/drivers/postgresql" // PostgreSQL driver
_ "github.com/rediwo/redi-orm/drivers/mongodb" // MongoDB driver
)
Creating Database Connections
Option 1: From URI (Recommended)
// SQLite
db, err := database.NewFromURI("sqlite://./myapp.db")
db, err := database.NewFromURI("sqlite://:memory:")
// MySQL
db, err := database.NewFromURI("mysql://user:pass@localhost:3306/mydb")
db, err := database.NewFromURI("mysql://user:pass@localhost/mydb?charset=utf8mb4&parseTime=true")
// PostgreSQL
db, err := database.NewFromURI("postgresql://user:pass@localhost:5432/mydb")
db, err := database.NewFromURI("postgres://user:pass@localhost/mydb?sslmode=require")
// MongoDB
db, err := database.NewFromURI("mongodb://user:pass@localhost:27017/mydb")
db, err := database.NewFromURI("mongodb+srv://cluster.mongodb.net/mydb")
Option 2: Direct Driver Instantiation
import (
"github.com/rediwo/redi-orm/drivers/sqlite"
"github.com/rediwo/redi-orm/drivers/mysql"
"github.com/rediwo/redi-orm/drivers/postgresql"
"github.com/rediwo/redi-orm/drivers/mongodb"
)
// SQLite - pass native file path
sqliteDB, err := sqlite.NewSQLiteDB("./myapp.db")
// MySQL - pass native DSN
mysqlDB, err := mysql.NewMySQLDB("user:pass@tcp(localhost:3306)/mydb?charset=utf8mb4&parseTime=true")
// PostgreSQL - pass native DSN
pgDB, err := postgresql.NewPostgreSQLDB("host=localhost user=user password=pass dbname=mydb")
// MongoDB - pass native connection string
mongoDB, err := mongodb.NewMongoDB("mongodb://user:pass@localhost:27017/mydb")
Connection URIs and Options
SQLite URIs
// File database
"sqlite://./app.db"
"sqlite:///absolute/path/to/db.sqlite"
// In-memory database
"sqlite://:memory:"
SQLite doesn't support query parameters in URIs but accepts PRAGMA statements after connection.
MySQL URIs
// Basic connection
"mysql://user:password@localhost:3306/database"
// With options
"mysql://user:pass@localhost/mydb?charset=utf8mb4&parseTime=true&timeout=10s"
// Common MySQL options:
// - charset=utf8mb4 (character set)
// - parseTime=true (parse TIME/DATE values)
// - timeout=10s (connection timeout)
// - readTimeout=30s (I/O read timeout)
// - writeTimeout=30s (I/O write timeout)
// - tls=true|false|skip-verify (TLS configuration)
// - interpolateParams=true (client-side parameter interpolation)
// - multiStatements=true (allow multiple statements)
PostgreSQL URIs
// Basic connection
"postgresql://user:password@localhost:5432/database"
"postgres://user:password@localhost:5432/database" // Alternative scheme
// With options
"postgresql://user:pass@localhost/mydb?sslmode=require&connect_timeout=10"
// Common PostgreSQL options:
// - sslmode=disable|require|verify-ca|verify-full
// - connect_timeout=10 (connection timeout in seconds)
// - application_name=myapp (application name)
// - search_path=schema1,schema2 (schema search path)
// - timezone=UTC (session timezone)
// - client_encoding=UTF8 (client encoding)
MongoDB URIs
// Basic connection
"mongodb://localhost:27017/database"
"mongodb://user:password@localhost:27017/database"
// Replica set
"mongodb://host1:27017,host2:27017,host3:27017/database?replicaSet=myRS"
// MongoDB Atlas (SRV)
"mongodb+srv://user:password@cluster.mongodb.net/database"
// Common MongoDB options:
// - authSource=admin (authentication database)
// - replicaSet=myRS (replica set name)
// - tls=true (enable TLS/SSL)
// - readPreference=primary|secondary|nearest
// - w=majority (write concern)
// - retryWrites=true (automatic retry on network errors)
Schema Management
Schema Definition
Define schemas using the Go native API:
import "github.com/rediwo/redi-orm/schema"
// Create a new schema
userSchema := schema.New("User").
AddField(schema.Field{
Name: "id",
Type: schema.FieldTypeInt,
PrimaryKey: true,
AutoIncrement: true,
}).
AddField(schema.Field{
Name: "email",
Type: schema.FieldTypeString,
Unique: true,
Nullable: false,
}).
AddField(schema.Field{
Name: "name",
Type: schema.FieldTypeString,
Nullable: false,
}).
AddField(schema.Field{
Name: "age",
Type: schema.FieldTypeInt,
Nullable: true,
}).
AddField(schema.Field{
Name: "createdAt",
Type: schema.FieldTypeDateTime,
Default: "now()",
Map: "created_at", // Custom column name
})
Field Types
// Available field types
schema.FieldTypeString // TEXT/VARCHAR
schema.FieldTypeInt // INTEGER/INT
schema.FieldTypeInt64 // BIGINT
schema.FieldTypeFloat // REAL/FLOAT
schema.FieldTypeBool // BOOLEAN (INTEGER in SQLite)
schema.FieldTypeDateTime // DATETIME/TIMESTAMP
schema.FieldTypeJSON // JSON (TEXT in SQLite)
schema.FieldTypeDecimal // DECIMAL/NUMERIC
Field Attributes
schema.Field{
Name: "fieldName", // Field name in schema
Type: schema.FieldTypeString,
PrimaryKey: true, // Primary key field
AutoIncrement: true, // Auto-increment (integers only)
Unique: true, // Unique constraint
Nullable: false, // NOT NULL constraint
Default: "defaultValue", // Default value
Map: "column_name", // Custom column name (@map annotation)
Index: true, // Create index
}
Composite Primary Keys
postTagSchema := schema.New("PostTag").
AddField(schema.Field{Name: "postId", Type: schema.FieldTypeInt}).
AddField(schema.Field{Name: "tagId", Type: schema.FieldTypeInt})
// Set composite primary key
postTagSchema.CompositeKey = []string{"postId", "tagId"}
Relations
// One-to-many relation (User has many Posts)
userSchema.AddRelation("posts", schema.Relation{
Type: schema.RelationOneToMany,
Model: "Post",
ForeignKey: "userId",
References: "id",
})
// Many-to-one relation (Post belongs to User)
postSchema.AddRelation("user", schema.Relation{
Type: schema.RelationManyToOne,
Model: "User",
ForeignKey: "userId",
References: "id",
OnDelete: "CASCADE", // Optional: CASCADE, SET NULL, RESTRICT
OnUpdate: "CASCADE", // Optional: CASCADE, SET NULL, RESTRICT
})
// Many-to-many relation (Post has many Tags through PostTag)
postSchema.AddRelation("tags", schema.Relation{
Type: schema.RelationManyToMany,
Model: "Tag",
Through: "PostTag",
ForeignKey: "postId",
References: "id",
})
Loading and Syncing Schemas
ctx := context.Background()
// Add schema to database
db.AddSchema(userSchema)
db.AddSchema(postSchema)
// Sync all schemas with database (creates tables)
if err := db.SyncSchemas(ctx); err != nil {
log.Fatal(err)
}
// Alternative: Sync individual schema
if err := db.CreateModel(ctx, "User"); err != nil {
log.Fatal(err)
}
Query Builder API
Model Queries
Get a model query builder:
// Get model query builder
User := db.Model("User")
Post := db.Model("Post")
Insert Operations
// Single insert
result, err := User.Insert(map[string]any{
"name": "John Doe",
"email": "john@example.com",
"age": 30,
}).Exec(ctx)
if err != nil {
log.Fatal(err)
}
// Get inserted ID (if auto-increment)
id, err := result.LastInsertId()
rowsAffected, err := result.RowsAffected()
// Insert with RETURNING (PostgreSQL, SQLite with RETURNING support)
var insertedUser map[string]any
err = User.Insert(map[string]any{
"name": "Jane Doe",
"email": "jane@example.com",
}).Returning("id", "name", "createdAt").Scan(ctx, &insertedUser)
// Batch insert
users := []map[string]any{
{"name": "Alice", "email": "alice@example.com"},
{"name": "Bob", "email": "bob@example.com"},
}
result, err := User.InsertMany(users).Exec(ctx)
Select Operations
// Find single record
var user map[string]any
err := User.Select().Where("id", 1).FindOne(ctx, &user)
// Find multiple records
var users []map[string]any
err := User.Select().Where("age", ">", 18).FindMany(ctx, &users)
// Scan into structs
type UserStruct struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}
var userStruct UserStruct
err := User.Select().Where("id", 1).FindOne(ctx, &userStruct)
var userStructs []UserStruct
err := User.Select().FindMany(ctx, &userStructs)
Where Conditions
// Simple conditions
User.Select().Where("name", "John") // name = 'John'
User.Select().Where("age", ">", 18) // age > 18
User.Select().Where("email", "!=", "test") // email != 'test'
// Multiple conditions (AND)
User.Select().
Where("age", ">", 18).
Where("name", "LIKE", "John%")
// OR conditions
User.Select().
Where("age", ">", 65).
OrWhere("status", "premium")
// IN conditions
User.Select().WhereIn("id", []any{1, 2, 3, 4})
User.Select().WhereNotIn("status", []any{"banned", "inactive"})
// NULL conditions
User.Select().WhereNull("deletedAt")
User.Select().WhereNotNull("emailVerifiedAt")
// BETWEEN conditions
User.Select().WhereBetween("age", 18, 65)
User.Select().WhereNotBetween("created_at", "2023-01-01", "2023-12-31")
Ordering and Limiting
// Order by
User.Select().OrderBy("name", "ASC")
User.Select().OrderBy("createdAt", "DESC")
// Multiple order conditions
User.Select().
OrderBy("status", "ASC").
OrderBy("createdAt", "DESC")
// Limit and offset
User.Select().Limit(10)
User.Select().Limit(10).Offset(20) // Pagination
// Count
count, err := User.Select().Where("active", true).Count(ctx)
Update Operations
// Update with WHERE
result, err := User.Update(map[string]any{
"name": "John Smith",
"updatedAt": "now()",
}).Where("id", 1).Exec(ctx)
rowsAffected, err := result.RowsAffected()
// Update with RETURNING
var updatedUser map[string]any
err = User.Update(map[string]any{
"name": "Jane Smith",
}).Where("id", 1).Returning("id", "name", "updatedAt").Scan(ctx, &updatedUser)
// Batch update
result, err := User.Update(map[string]any{
"status": "inactive",
}).Where("lastLoginAt", "<", "2023-01-01").Exec(ctx)
Delete Operations
// Delete with WHERE
result, err := User.Delete().Where("id", 1).Exec(ctx)
rowsAffected, err := result.RowsAffected()
// Delete with RETURNING
var deletedUser map[string]any
err = User.Delete().Where("id", 1).Returning("id", "name").Scan(ctx, &deletedUser)
// Batch delete
result, err := User.Delete().Where("active", false).Exec(ctx)
// Delete all (use with caution)
result, err := User.Delete().Exec(ctx)
Eager Loading (Relations)
// Include related data
var usersWithPosts []map[string]any
err := User.Select().Include("posts").FindMany(ctx, &usersWithPosts)
// Nested includes
var postsWithAuthorAndTags []map[string]any
err := Post.Select().
Include("author").
Include("tags").
FindMany(ctx, &postsWithAuthorAndTags)
// Conditional includes
var users []map[string]any
err := User.Select().
Include("posts", func(q types.SelectQuery) types.SelectQuery {
return q.Where("published", true).OrderBy("createdAt", "DESC")
}).
FindMany(ctx, &users)
Raw SQL Queries
Raw Queries
// SQL queries (works with all databases including MongoDB)
rawQuery := db.Raw("SELECT * FROM users WHERE age > ?", 18)
var users []map[string]any
err := rawQuery.Scan(ctx, &users)
// Scan single row
var user map[string]any
err := db.Raw("SELECT * FROM users WHERE id = ?", 1).ScanOne(ctx, &user)
// Execute non-SELECT queries
result, err := db.Raw("UPDATE users SET status = ? WHERE age < ?", "minor", 18).Exec(ctx)
rowsAffected, err := result.RowsAffected()
// MongoDB: Native commands (JSON format)
rawQuery := db.Raw(`{
"find": "users",
"filter": {"age": {"$gt": 18}},
"sort": {"name": 1}
}`)
var users []map[string]any
err := rawQuery.Scan(ctx, &users)
// MongoDB: Aggregation pipeline
rawQuery := db.Raw(`{
"aggregate": "users",
"pipeline": [
{"$match": {"age": {"$gte": 18}}},
{"$group": {
"_id": "$city",
"count": {"$sum": 1}
}}
]
}`)
var results []map[string]any
err := rawQuery.Scan(ctx, &results)
Using Database/SQL Directly
Access the underlying *sql.DB:
// Get underlying database connection
sqlDB := db.GetDB()
// Standard database/sql operations
rows, err := sqlDB.QueryContext(ctx, "SELECT * FROM users")
defer rows.Close()
for rows.Next() {
var id int
var name, email string
err := rows.Scan(&id, &name, &email)
// Process row...
}
Transactions
Transaction API
// Execute function in transaction
err := db.Transaction(ctx, func(tx types.Transaction) error {
// All operations use tx instead of db
// Insert user
userResult, err := tx.Model("User").Insert(map[string]any{
"name": "John Doe",
"email": "john@example.com",
}).Exec(ctx)
if err != nil {
return err // Automatic rollback
}
userID, _ := userResult.LastInsertId()
// Insert related post
_, err = tx.Model("Post").Insert(map[string]any{
"title": "Hello World",
"userId": userID,
}).Exec(ctx)
if err != nil {
return err // Automatic rollback
}
// If no error, transaction commits automatically
return nil
})
if err != nil {
log.Printf("Transaction failed: %v", err)
}
Manual Transaction Control
// Begin transaction manually
tx, err := db.Begin(ctx)
if err != nil {
log.Fatal(err)
}
// Always rollback on defer (no-op if already committed)
defer tx.Rollback(ctx)
// Perform operations
_, err = tx.Model("User").Insert(userData).Exec(ctx)
if err != nil {
// tx.Rollback(ctx) called by defer
return err
}
_, err = tx.Model("Post").Insert(postData).Exec(ctx)
if err != nil {
// tx.Rollback(ctx) called by defer
return err
}
// Commit transaction
if err := tx.Commit(ctx); err != nil {
return err
}
Savepoints
err := db.Transaction(ctx, func(tx types.Transaction) error {
// Insert user
_, err := tx.Model("User").Insert(userData).Exec(ctx)
if err != nil {
return err
}
// Create savepoint
savepoint := "user_posts"
if err := tx.Savepoint(ctx, savepoint); err != nil {
return err
}
// Try to insert posts
for _, postData := range posts {
_, err := tx.Model("Post").Insert(postData).Exec(ctx)
if err != nil {
// Rollback to savepoint (keep user, discard posts)
if rollbackErr := tx.RollbackToSavepoint(ctx, savepoint); rollbackErr != nil {
return rollbackErr
}
break
}
}
return nil
})
Migration System
Database Migrator
// Get migrator for database
migrator := db.GetMigrator()
// Check if migration table exists
exists, err := migrator.MigrationTableExists(ctx)
// Create migration table
if !exists {
err := migrator.CreateMigrationTable(ctx)
}
// Apply migration
migration := &types.Migration{
Name: "create_users_table",
UpSQL: `CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)`,
DownSQL: `DROP TABLE users`,
}
err := migrator.ApplyMigration(ctx, migration)
// Rollback migration
err := migrator.RollbackMigration(ctx, "create_users_table")
// Get migration status
applied, err := migrator.GetAppliedMigrations(ctx)
for _, migration := range applied {
fmt.Printf("Applied: %s at %s\n", migration.Name, migration.AppliedAt)
}
Schema Comparison
// Compare current database schema with loaded schemas
differences, err := migrator.CompareSchemas(ctx)
for _, diff := range differences {
switch diff.Type {
case types.DifferenceTypeTableMissing:
fmt.Printf("Table missing: %s\n", diff.TableName)
case types.DifferenceTypeColumnMissing:
fmt.Printf("Column missing: %s.%s\n", diff.TableName, diff.ColumnName)
case types.DifferenceTypeColumnTypeMismatch:
fmt.Printf("Column type mismatch: %s.%s\n", diff.TableName, diff.ColumnName)
}
}
Driver Capabilities
Checking Driver Capabilities
caps := db.GetCapabilities()
// Check if driver supports RETURNING clause
if caps.SupportsReturning() {
// Use RETURNING in INSERT/UPDATE/DELETE
var result map[string]any
err := User.Insert(data).Returning("id", "name").Scan(ctx, &result)
}
// Check if driver requires LIMIT for OFFSET
if caps.RequiresLimitForOffset() {
// SQLite requires LIMIT when using OFFSET
query = query.Limit(1000000).Offset(20)
} else {
// MySQL/PostgreSQL can use OFFSET alone
query = query.Offset(20)
}
// Get NULLS ordering SQL
nullsSQL := caps.GetNullsOrderingSQL("ASC", "LAST")
// Returns " NULLS LAST" for PostgreSQL/SQLite, "" for MySQL
Driver-Specific Capabilities
Each driver has different capabilities:
SQLite:
- Supports RETURNING (in newer versions)
- Requires LIMIT when using OFFSET
- Supports NULLS FIRST/LAST ordering
- Auto-increment uses AUTOINCREMENT
MySQL:
- Limited RETURNING support (MySQL 8.0+)
- Doesn't require LIMIT for OFFSET
- No NULLS ordering support
- Auto-increment uses AUTO_INCREMENT
PostgreSQL:
- Full RETURNING support
- Doesn't require LIMIT for OFFSET
- Full NULLS ordering support
- Auto-increment uses SERIAL/BIGSERIAL
MongoDB:
- No RETURNING support (uses find-then-modify pattern)
- No LIMIT requirement for OFFSET (uses skip)
- No NULLS ordering (null values sort differently)
- Auto-increment uses custom sequence collection
Type Conversion Utilities
Handle driver-specific type differences:
import "github.com/rediwo/redi-orm/utils"
// Safe type conversions
boolValue := utils.ToBool(result["active"]) // Handles int64 from SQLite
intValue := utils.ToInt64(result["count"]) // Handles string from MySQL
floatValue := utils.ToFloat64(result["price"]) // Handles various numeric types
stringValue := utils.ToString(result["name"]) // Handles []byte, string, etc.
// Other conversions
intValue := utils.ToInt(result["age"])
floatValue := utils.ToFloat32(result["score"])
interfaceValue := utils.ToInterface(result["data"]) // Normalizes driver-specific types
Error Handling
Database-Specific Error Handling
import (
"database/sql"
"errors"
"strings"
)
// Check for common errors
if err != nil {
// No rows found
if errors.Is(err, sql.ErrNoRows) {
// Handle not found
return nil, fmt.Errorf("user not found")
}
// Constraint violations (driver-specific)
if strings.Contains(err.Error(), "UNIQUE constraint") { // SQLite
return nil, fmt.Errorf("email already exists")
}
if strings.Contains(err.Error(), "Duplicate entry") { // MySQL
return nil, fmt.Errorf("email already exists")
}
if strings.Contains(err.Error(), "duplicate key") { // PostgreSQL
return nil, fmt.Errorf("email already exists")
}
// Other database errors
return nil, fmt.Errorf("database error: %w", err)
}
Connection Error Handling
// Test database connection
if err := db.Ping(ctx); err != nil {
log.Printf("Database connection failed: %v", err)
// Attempt reconnection
if err := db.Connect(ctx); err != nil {
log.Fatal("Failed to reconnect to database")
}
}
Best Practices
1. Resource Management
// Always close database connections
defer db.Close()
// Use context with timeouts
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Close result sets
rows, err := db.Raw("SELECT * FROM users").Query(ctx)
if err != nil {
return err
}
defer rows.Close() // Important!
2. Connection Pooling
// Configure connection pool (after Connect)
sqlDB := db.GetDB()
sqlDB.SetMaxOpenConns(25) // Maximum open connections
sqlDB.SetMaxIdleConns(25) // Maximum idle connections
sqlDB.SetConnMaxLifetime(5 * time.Minute) // Connection lifetime
3. Prepared Statements
// Raw queries automatically use prepared statements
for _, userID := range userIDs {
var user map[string]any
err := db.Raw("SELECT * FROM users WHERE id = ?", userID).ScanOne(ctx, &user)
if err != nil {
log.Printf("Failed to get user %d: %v", userID, err)
}
}
// Model queries also use prepared statements internally
for _, userData := range users {
_, err := User.Insert(userData).Exec(ctx)
if err != nil {
log.Printf("Failed to insert user: %v", err)
}
}
4. Error Recovery
func performDatabaseOperation() error {
maxRetries := 3
for i := 0; i < maxRetries; i++ {
err := db.Transaction(ctx, func(tx types.Transaction) error {
// Database operations...
return nil
})
if err == nil {
return nil // Success
}
// Check if error is retryable
if isRetryableError(err) && i < maxRetries-1 {
time.Sleep(time.Duration(i+1) * time.Second)
continue
}
return err
}
return nil
}
func isRetryableError(err error) bool {
errStr := err.Error()
return strings.Contains(errStr, "connection") ||
strings.Contains(errStr, "timeout") ||
strings.Contains(errStr, "deadlock")
}
Testing
Test Database Setup
func setupTestDB(t *testing.T) types.Database {
ctx := context.Background()
// Use in-memory SQLite for tests
db, err := database.NewFromURI("sqlite://:memory:")
require.NoError(t, err)
err = db.Connect(ctx)
require.NoError(t, err)
// Load test schemas
db.AddSchema(userSchema)
db.AddSchema(postSchema)
err = db.SyncSchemas(ctx)
require.NoError(t, err)
t.Cleanup(func() {
db.Close()
})
return db
}
func TestUserOperations(t *testing.T) {
db := setupTestDB(t)
ctx := context.Background()
// Test operations...
User := db.Model("User")
result, err := User.Insert(map[string]any{
"name": "Test User",
"email": "test@example.com",
}).Exec(ctx)
assert.NoError(t, err)
id, err := result.LastInsertId()
assert.NoError(t, err)
assert.Greater(t, id, int64(0))
}
Mock Testing
// For unit testing without database
import "github.com/rediwo/redi-orm/test/mocks"
func TestBusinessLogic(t *testing.T) {
mockDB := mocks.NewMockDatabase()
// Configure mock behavior
mockDB.On("Model", "User").Return(mocks.NewMockModelQuery())
// Test your business logic
service := NewUserService(mockDB)
user, err := service.CreateUser("John", "john@example.com")
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
mockDB.AssertExpectations(t)
}
This covers the comprehensive low-level driver API. For higher-level operations and rapid development, use the ORM API instead.
Click to show internal directories.
Click to hide internal directories.