libsql-orm 0.1.0

A powerful, async-first ORM for libsql with first-class support for Cloudflare Workers and WebAssembly environments. Features include automatic boolean conversion, upsert operations, built-in logging, migrations, and comprehensive query building.
Documentation

libsql-orm

Crates.io Documentation License Build Status

A powerful, async-first ORM for libsql with first-class support for Cloudflare Workers and WebAssembly environments.

✨ Features

  • 🚀 Cloudflare Workers Ready - Built specifically for edge computing environments
  • 🔄 Async/Await Support - Fully async API with excellent performance
  • 🎯 Type-Safe - Leverages Rust's type system for compile-time safety
  • 📊 Rich Query Builder - Fluent API for complex queries
  • 🔍 Advanced Filtering - Search, pagination, sorting, and aggregations
  • 🛠️ Migration System - Database schema management and versioning
  • 🎨 Derive Macros - Automatic model generation with #[derive(Model)]
  • 📦 Bulk Operations - Efficient batch inserts, updates, and deletes
  • 🌐 WASM Compatible - Optimized for WebAssembly targets
  • 🔧 Custom Table Names - #[table_name("custom")] attribute support
  • Boolean Type Safety - Automatic SQLite integer ↔ Rust boolean conversion
  • 🏷️ Column Attributes - #[orm_column(...)] for column customization
  • 🔄 Upsert Operations - Smart create_or_update and upsert methods
  • 📝 Built-in Logging - Comprehensive logging for debugging and monitoring

🚀 Quick Start

Add this to your Cargo.toml:

[dependencies]
libsql-orm = "0.1.0"
serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }

Basic Usage

use libsql_orm::{Model, Database};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Model, Debug, Clone, Serialize, Deserialize)]
#[table_name("users")]  // Custom table name (optional)
struct User {
    pub id: Option<i64>,
    pub name: String,
    pub email: String,
    pub age: Option<i32>,
    pub is_active: bool,        // ✅ Automatic boolean conversion
    pub is_verified: bool,      // ✅ Works with any boolean field
    pub created_at: DateTime<Utc>,
}

// In your async function
async fn example() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to database
    let db = Database::new_connect("libsql://your-db.turso.io", "your-auth-token").await?;
    
    // Create a user
    let user = User {
        id: None,
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
        age: Some(30),
        is_active: true,
        created_at: Utc::now(),
    };
    
    // Save to database
    let saved_user = user.create(&db).await?;
    println!("Created user with ID: {:?}", saved_user.id);
    
    // Find users
    let users = User::find_all(&db).await?;
    println!("Found {} users", users.len());
    
    // Query with conditions
    let active_users = User::find_where(
        FilterOperator::Eq("is_active".to_string(), crate::Value::Boolean(true)),
        &db
    ).await?;
    
    Ok(())
}

Cloudflare Workers Integration

use worker::*;
use libsql_orm::{Model, Database, MigrationManager, generate_migration};

#[derive(Model, Debug, Clone, Serialize, Deserialize)]
#[table_name("blog_posts")]  // Custom table name
struct Post {
    pub id: Option<i64>,
    pub title: String,
    pub content: String,
    pub published: bool,       // ✅ Boolean automatically converted from SQLite
    pub featured: bool,        // ✅ Multiple boolean fields supported
    pub created_at: DateTime<Utc>,
}

#[event(fetch)]
async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
    console_error_panic_hook::set_once();
    
    // Get database credentials from environment
    let database_url = env.var("LIBSQL_DATABASE_URL")?.to_string();
    let auth_token = env.var("LIBSQL_AUTH_TOKEN")?.to_string();
    
    // Connect to database
    let db = Database::new_connect(&database_url, &auth_token).await
        .map_err(|e| format!("Database connection failed: {}", e))?;
    
    // Run migrations
    let manager = MigrationManager::new(db);
    let migration = generate_migration!(Post);
    manager.execute_migration(&migration).await
        .map_err(|e| format!("Migration failed: {}", e))?;
    
    let db = manager.database();
    
    // Handle the request
    match req.method() {
        Method::Get => {
            let posts = Post::find_all(db).await
                .map_err(|e| format!("Query failed: {}", e))?;
            Response::from_json(&posts)
        }
        Method::Post => {
            let post: Post = req.json().await?;
            let saved_post = post.create(db).await
                .map_err(|e| format!("Create failed: {}", e))?;
            Response::from_json(&saved_post)
        }
        _ => Response::error("Method not allowed", 405)
    }
}

📚 Advanced Features

Custom Table Names

Use the #[table_name("custom_name")] attribute to specify custom table names:

#[derive(Model, Serialize, Deserialize)]
#[table_name("user_accounts")]  // Custom table name
struct User {
    pub id: Option<i64>,
    pub username: String,
    pub email: String,
}

// Default table name would be "user" (struct name lowercase)
// With attribute, table name is "user_accounts"
assert_eq!(User::table_name(), "user_accounts");

Benefits:

  • 🏷️ Legacy Integration - Map to existing database tables
  • 🎯 Naming Control - Override default naming conventions
  • 📁 Multi-tenant - Use prefixes like tenant_users
  • 🔄 Migration Friendly - Rename tables without changing structs

Boolean Type Safety

libsql-orm automatically handles boolean conversion between SQLite and Rust:

#[derive(Model, Serialize, Deserialize)]
struct User {
    pub id: Option<i64>,
    pub is_active: bool,      // ✅ SQLite INTEGER(0/1) ↔ Rust bool
    pub is_verified: bool,    // ✅ Automatic conversion
    pub has_premium: bool,    // ✅ Works with any boolean field name
    pub can_edit: bool,       // ✅ No configuration needed
    pub enabled: bool,        // ✅ Type-safe operations
}

// All boolean operations work seamlessly
let user = User::find_where(
    FilterOperator::Eq("is_active".to_string(), Value::Boolean(true)),
    &db
).await?;

// JSON serialization works correctly
let json = serde_json::to_string(&user)?;  // ✅ Booleans as true/false
let deserialized: User = serde_json::from_str(&json)?;  // ✅ No errors

Key Features:

  • Automatic Detection - Boolean fields identified at compile time
  • Zero Configuration - Works with any boolean field name
  • Type Safety - No runtime errors or invalid conversions
  • Performance - Conversion logic generated at compile time
  • JSON Compatible - Seamless serialization/deserialization

Column Attributes

Customize column properties with #[orm_column(...)]:

#[derive(Model, Serialize, Deserialize)]
struct Product {
    #[orm_column(type = "INTEGER PRIMARY KEY AUTOINCREMENT")]
    pub id: Option<i64>,
    
    #[orm_column(not_null, unique)]
    pub sku: String,
    
    #[orm_column(type = "REAL CHECK(price >= 0)")]
    pub price: f64,
    
    #[orm_column(type = "BOOLEAN DEFAULT TRUE")]
    pub is_available: bool,     // ✅ Boolean with DEFAULT constraint
}

Query Builder

use libsql_orm::{QueryBuilder, FilterOperator, Sort, SortOrder, Pagination};

// Complex query with filtering and pagination
let query = QueryBuilder::new("users")
    .select(&["id", "name", "email"])
    .r#where(FilterOperator::Gte("age".to_string(), Value::Integer(18)))
    .order_by(Sort::new("created_at", SortOrder::Desc))
    .limit(10)
    .offset(20);

let (sql, params) = query.build()?;

Pagination

use libsql_orm::{Pagination, PaginatedResult};

let pagination = Pagination::new(1, 10); // page 1, 10 items per page
let result: PaginatedResult<User> = User::find_paginated(&pagination, &db).await?;

println!("Page {}/{}", result.current_page, result.total_pages);
println!("Total items: {}", result.total_count);
for user in result.data {
    println!("User: {}", user.name);
}

Bulk Operations

// Bulk insert
let users = vec![
    User { /* ... */ },
    User { /* ... */ },
    User { /* ... */ },
];
let saved_users = User::bulk_create(&users, &db).await?;

// Bulk delete
let ids_to_delete = vec![1, 2, 3, 4, 5];
let deleted_count = User::bulk_delete(&ids_to_delete, &db).await?;

Aggregations

use libsql_orm::Aggregate;

// Count users
let total_users = User::count(&db).await?;

// Average age
let avg_age = User::aggregate(
    Aggregate::Avg,
    "age",
    None,
    &db
).await?;

// Count with filter
let active_users_count = User::count_where(
    FilterOperator::Eq("is_active".to_string(), Value::Boolean(true)),
    &db
).await?;

Search

use libsql_orm::SearchFilter;

let search = SearchFilter::new(
    vec!["name".to_string(), "email".to_string()],
    "john".to_string()
);

let results = User::search(&search, Some(&pagination), &db).await?;

Upsert Operations

libsql-orm provides intelligent create-or-update operations:

use libsql_orm::{Model, Database};

// Create or update based on primary key
let mut user = User {
    id: Some(123),  // If record exists, it will be updated
    name: "John Doe".to_string(),
    email: "john@example.com".to_string(),
    is_active: true,
    created_at: Utc::now(),
};

// Automatically decides whether to create or update
let saved_user = user.create_or_update(&db).await?;

// Upsert based on unique constraints (e.g., email)
let user = User {
    id: None,  // Primary key not set
    name: "Jane Smith".to_string(),
    email: "jane@example.com".to_string(),  // Unique field
    is_active: true,
    created_at: Utc::now(),
};

// Will update existing record with this email, or create new if not found
let saved_user = user.upsert(&["email"], &db).await?;

// Multiple unique constraints
let saved_user = user.upsert(&["email", "username"], &db).await?;

Use Cases:

  • Data Synchronization - Import external data without duplicates
  • User Registration - Update existing accounts or create new ones
  • Configuration Management - Maintain settings without conflicts
  • API Endpoints - Handle PUT requests efficiently

Built-in Logging

libsql-orm includes comprehensive logging for debugging and monitoring:

use libsql_orm::{Model, Database};

// All operations are automatically logged
let user = User::new("John", "john@example.com");

// Logs: [INFO] users: Creating record in table: users
// Logs: [DEBUG] users: SQL: INSERT INTO users (name, email, is_active) VALUES (?, ?, ?)
// Logs: [INFO] users: Successfully created record with ID: 123
let saved_user = user.create(&db).await?;

// Logs: [DEBUG] users: Finding record by ID: 123
// Logs: [INFO] users: Found record with ID: 123
let found_user = User::find_by_id(123, &db).await?;

// Logs: [INFO] users: Updating existing record with ID: 123
// Logs: [INFO] users: Updating record with ID: 123
// Logs: [DEBUG] users: SQL: UPDATE users SET name = ?, email = ? WHERE id = ?
// Logs: [INFO] users: Successfully updated record with ID: 123
let updated_user = found_user.unwrap().create_or_update(&db).await?;

Logging Features:

  • 🎯 Cross-Platform - Uses browser console in WASM, standard logging elsewhere
  • 📊 Multiple Levels - INFO, DEBUG, WARN, ERROR levels
  • 🏷️ Table Context - Automatic table name prefixing for clarity
  • 🔍 SQL Debugging - View actual SQL queries being executed
  • Performance Friendly - Minimal overhead in production

Cloudflare Workers Logging:

// In browser/worker environment, logs appear in console
// [INFO] users: Creating record in table: users
// [DEBUG] users: SQL: INSERT INTO users (...) VALUES (...)
// [WARN] users: Record with ID 999 not found, creating new record

Native Application Logging:

// Configure logging in your application
use log::LevelFilter;
use env_logger;

env_logger::Builder::from_default_env()
    .filter_level(LevelFilter::Debug)
    .init();

// Now all ORM operations will use standard Rust logging

🔧 Migrations

libsql-orm includes a powerful migration system:

use libsql_orm::{MigrationManager, Migration, MigrationBuilder};

// Auto-generate migration from model
let migration = generate_migration!(User);

// Or create manually
let migration = MigrationBuilder::new("create_users_table")
    .up(r#"
        CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            email TEXT UNIQUE NOT NULL,
            created_at TEXT NOT NULL
        )
    "#)
    .down("DROP TABLE users")
    .build();

// Execute migration
let manager = MigrationManager::new(db);
manager.execute_migration(&migration).await?;

🏗️ Architecture

WASM Compatibility

libsql-orm is built from the ground up for WebAssembly environments:

  • Uses libsql WASM bindings for database connectivity
  • Optimized async runtime for edge computing
  • Minimal binary size with selective feature compilation
  • Compatible with Cloudflare Workers, Deno Deploy, and other edge platforms

Performance

  • Zero-copy deserialization where possible
  • Connection pooling for optimal database usage
  • Lazy loading of related data
  • Efficient batch operations for bulk data handling

📖 Examples

Complete CRUD Example

use libsql_orm::{Model, Database, FilterOperator, Pagination};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Model, Debug, Clone, Serialize, Deserialize)]
struct User {
    pub id: Option<i64>,
    pub name: String,
    pub email: String,
    pub age: Option<i32>,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
}

async fn crud_example() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::new_connect("your-db-url", "your-token").await?;
    
    // CREATE
    let user = User {
        id: None,
        name: "John Doe".to_string(),
        email: "john@example.com".to_string(),
        age: Some(30),
        is_active: true,
        created_at: Utc::now(),
    };
    let created_user = user.create(&db).await?;
    
    // READ
    let found_user = User::find_by_id(created_user.id.unwrap(), &db).await?;
    let all_users = User::find_all(&db).await?;
    
    // UPDATE
    let mut user_to_update = found_user.unwrap();
    user_to_update.name = "Jane Doe".to_string();
    let updated_user = user_to_update.update(&db).await?;
    
    // CREATE OR UPDATE (smart upsert)
    let user_with_id = User {
        id: Some(999),  // If exists, update; if not, create
        name: "Smart User".to_string(),
        email: "smart@example.com".to_string(),
        age: Some(25),
        is_active: true,
        created_at: Utc::now(),
    };
    let smart_saved = user_with_id.create_or_update(&db).await?;
    
    // UPSERT by unique constraint
    let unique_user = User {
        id: None,
        name: "Unique User".to_string(),
        email: "unique@example.com".to_string(),  // Will check if this email exists
        age: Some(35),
        is_active: true,
        created_at: Utc::now(),
    };
    let upserted_user = unique_user.upsert(&["email"], &db).await?;
    
    // DELETE
    updated_user.delete(&db).await?;
    
    Ok(())
}

🔗 Ecosystem

libsql-orm works great with:

🔧 Troubleshooting

Boolean Serialization Issues

If you encounter errors like "invalid type: integer '1', expected a boolean", you have two solutions:

Option 1: Automatic Conversion (Recommended)

The derive macro handles this automatically in most cases:

// ✅ This works automatically with the derive macro
#[derive(Model, Serialize, Deserialize)]
struct User {
    pub is_active: bool,    // Automatically converts SQLite 0/1 to false/true
    pub enabled: bool,      // Works with any boolean field name
}

Option 2: Manual Deserializer (For Edge Cases)

If automatic conversion doesn't work, use the custom deserializer:

use libsql_orm::deserialize_bool;

#[derive(Model, Serialize, Deserialize)]
struct User {
    pub id: Option<i64>,
    pub name: String,
    
    // Use custom deserializer for problematic boolean fields
    #[serde(deserialize_with = "deserialize_bool")]
    pub is_active: bool,
    
    #[serde(deserialize_with = "deserialize_bool")]
    pub is_verified: bool,
}

The deserialize_bool function handles:

  • Integers: 0false, 1true
  • Booleans: Pass through unchanged
  • Strings: "true", "1", "yes"true; "false", "0", "no"false

Table Name Conflicts

Use the #[table_name("custom")] attribute to resolve naming conflicts:

#[derive(Model, Serialize, Deserialize)]
#[table_name("app_users")]  // Avoid conflicts with system tables
struct User {
    pub id: Option<i64>,
    pub name: String,
}

🤝 Contributing

Contributions are welcome! Please read our Contributing Guide for details.

Development Setup

git clone https://github.com/your-username/libsql-orm
cd libsql-orm
cargo build
cargo test

Running Tests

# Unit tests
cargo test

# Integration tests with real database
cargo test --features integration-tests

# WASM tests
wasm-pack test --node

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • libsql team for the excellent database engine
  • Cloudflare for the Workers platform
  • Rust community for the amazing ecosystem

📊 Status

  • Stable API - Ready for production use
  • Well Tested - Comprehensive test suite
  • Documented - Complete API documentation
  • WASM Ready - Optimized for edge computing
  • 🔄 Active Development - Regular updates and improvements

Need help?