Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to d1-rs, the world's most advanced type-safe ORM that revolutionizes database interactions in Rust with unprecedented compile-time safety and performance!

What is d1-rs?

d1-rs is a revolutionary Object-Relational Mapping (ORM) library that is the first ORM in any language to provide compile-time safe relationships, nested eager loading, recursive relationships, and rich M2M entities. Designed specifically for Cloudflare D1, with first-class support for local SQLite testing.

🚀 Revolutionary Features - World Firsts

🏆 World's First Compile-Time Safe Nested Eager Loading

  • IMPOSSIBLE ERRORS: All nested relation names validated at compile-time
  • REVOLUTIONARY SYNTAX: User::query().with_posts(|posts| posts.with_categories()).all()
  • AUTOMATIC N+1 PREVENTION: Multi-level JOINs generated automatically
  • IDE AUTO-COMPLETION: Full IntelliSense support for nested relations

🔥 World's First Compile-Time Safe Recursive Relationships

  • TYPE-SAFE RECURSION: Self-referential relations with compile-time safety
  • INTELLIGENT HANDLING: Automatic null handling for optional foreign keys
  • UNLIMITED DEPTH: Tree structures, hierarchies, and self-referencing entities

🚀 World's First Rich M2M with Junction Entities

  • UNPRECEDENTED: Junction tables as first-class entities with full Entity powers
  • RICH DATA: Additional fields in M2M relationships (granted_by, expires_at, etc.)
  • DIRECT QUERYING: Query junction entities directly - no other ORM allows this!

🛡️ Ultimate Type Safety

  • ZERO STRING LITERALS: All field/relation names compile-time validated
  • IMPOSSIBLE ERRORS: Typos = compile errors, not runtime crashes
  • COMPILE-TIME VALIDATION: All relationships analyzed for consistency
  • ZERO RUNTIME OVERHEAD: All validation happens at compile-time

Dual Backend System

  • Production: Seamless Cloudflare D1 integration
  • Development: Local SQLite with in-memory testing
  • Zero configuration switching between environments

🎯 Superior Performance

  • MEMORY EFFICIENT: SQL COUNT(*) and LIMIT 1 queries, never load unnecessary data
  • OPTIMAL SQL GENERATION: Database operations, not in-memory processing
  • ZERO WASTE: Every query optimized for the specific operation

Why Choose d1-rs?

Perfect for Cloudflare Workers

Built from the ground up for Cloudflare's edge computing platform with D1 database integration.

Testing Made Easy

Write comprehensive tests using local SQLite without needing cloud database access.

Performance Focused

Conditional compilation ensures only necessary code is included for your target platform.

Type-Safe Relations

Advanced graph traversal system with eager loading and cycle prevention.

🔥 Revolutionary Quick Example

Experience the world's most advanced ORM capabilities:

use d1_rs::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Entity)]
pub struct User {
    #[primary_key] pub id: i64,
    pub name: String,
    pub email: String,
}

#[derive(Debug, Serialize, Deserialize, Entity)]
pub struct Post {
    #[primary_key] pub id: i64,
    pub user_id: i64,
    pub title: String,
    pub is_published: bool,
}

#[derive(Debug, Serialize, Deserialize, Entity)]
pub struct Category {
    #[primary_key] pub id: i64,
    pub name: String,
    pub parent_id: Option<i64>, // ✅ Recursive relationships!
}

// ✅ REVOLUTIONARY: Zero string literals, compile-time safe relationships
relations! {
    User { has_many posts: Post via user_id }
    Post { 
        belongs_to user: User via user_id,
        has_many post_categories: PostCategory via post_id,
    }
    Category {
        belongs_to parent: Category via parent_id,    // ✅ Recursive!
        has_many children: Category via parent_id,    // ✅ Self-referential!
        has_many post_categories: PostCategory via category_id,
    }
    PostCategory {  // ✅ Rich M2M junction entity!
        belongs_to post: Post via post_id,
        belongs_to category: Category via category_id,
    }
}

// 🏆 WORLD'S FIRST: Compile-time safe nested eager loading
let users_with_data = User::query()
    .with_posts(|posts| posts.with_categories())  // ✅ Impossible errors!
    .with_profile()
    .all(&db).await?;

// 🚀 REVOLUTIONARY: Type-safe recursive relationships
let category_hierarchy = root_category.children().all(&db).await?;
let parent = child_category.parent().first(&db).await?;

// 🔥 UNPRECEDENTED: Rich M2M with junction entities
let junction_records = PostCategory::query()
    .where_assigned_by_eq(admin_user.name)  // ✅ Type-safe field access!
    .where_is_primary_eq(true)              // ✅ Boolean type, not string!
    .all(&db).await?;

// Navigate through junction entities - IMPOSSIBLE in other ORMs!
let category_from_junction = junction.category().first(&db).await?;

🏗️ Revolutionary Architecture

d1-rs delivers world-first compile-time safe relationships through advanced architecture:

┌──────────────────────────────────────┐
│        🏆 WORLD'S MOST ADVANCED      │
│         COMPILE-TIME VALIDATION      │
│                                      │
│ ✅ Nested Eager Loading Validation   │
│ ✅ Recursive Relationship Analysis   │  
│ ✅ Junction Entity Type Checking     │
│ ✅ Zero String Literal Enforcement   │
└──────────────────┬───────────────────┘
                   │
┌─────────────────┐│   ┌──────────────────┐
│   Your Code     ││   │  🚀 d1-rs ORM    │
│                 ││───│  REVOLUTIONARY   │
│ Type-Safe Rust  ││   │ ● Entity System  │
│ Zero Strings!   ││   │ ● Query Builders │
└─────────────────┘│   │ ● Relations API  │
                   │   └──────────────────┘
                   │            │
                   │   ┌────────┴────────┐
                   │   │                 │
                   │┌──▼─────────────┐ ┌─▼──────┐
                   ││ Cloudflare D1  │ │ SQLite │
                   ││ (Production)   │ │(Testing)│
                   │└────────────────┘ └────────┘
                   │
        ┌──────────▼────────────┐
        │ 🧠 COMPILE-TIME MAGIC │
        │                       │
        │ ● All errors caught   │
        │   before runtime!     │
        │ ● Perfect IDE support │
        │ ● Zero runtime cost   │
        └───────────────────────┘

🏆 d1-rs vs Other ORMs - No Competition

Featured1-rsRails/ActiveRecordDjango ORMEloquentEnt-GoPrisma
Nested Eager Loading✅ Compile-time safe❌ Runtime strings❌ Runtime strings❌ Runtime strings❌ Runtime strings❌ Runtime strings
Type Safety✅ Full compile-time❌ Runtime only❌ Runtime only❌ Runtime only❌ Runtime only❌ Runtime only
Recursive Relations✅ Compile-time safe❌ Manual/limited❌ Manual/limited❌ Manual/limited❌ Manual/limited❌ Manual/limited
Rich M2M Junction✅ First-class entities❌ Limited❌ Limited❌ Limited❌ Limited❌ Limited
N+1 Prevention✅ Automatic❌ Manual includes❌ Manual select_related❌ Manual with❌ Manual preload❌ Manual include
Error Prevention✅ Impossible errors❌ Runtime crashes❌ Runtime crashes❌ Runtime crashes❌ Runtime crashes❌ Runtime crashes
IDE Support✅ Full auto-complete❌ String literals❌ String literals❌ String literals❌ String literals❌ String literals
Performance✅ Zero overhead❌ Runtime overhead❌ Runtime overhead❌ Runtime overhead❌ Runtime overhead❌ Runtime overhead

d1-rs is literally impossible to match - there is no competition! 🌟

Ready to Experience the Revolution?

Jump into the Installation guide to set up d1-rs in your project, or check out the Quick Start for a hands-on tutorial with the world's most advanced ORM!

Installation

Setting up d1-rs in your project is straightforward. This guide covers installation for both Cloudflare Workers and local development.

Requirements

  • Rust: 1.70.0 or later
  • Cargo: Latest stable version
  • wrangler (for Cloudflare Workers deployment)

Adding d1-rs to Your Project

Add d1-rs to your Cargo.toml:

[dependencies]
d1-rs = "0.1.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }

# For Cloudflare Workers
[target.'cfg(target_arch = "wasm32")'.dependencies]
worker = "0.0.18"

# For local development and testing  
[dev-dependencies]
rusqlite = "0.30"

Feature Flags

d1-rs uses conditional compilation to optimize for your target environment:

[dependencies]
d1-rs = { version = "0.1.0", features = ["sqlite"] }  # For testing

Available Features

FeatureDescriptionWhen to Use
sqliteEnable SQLite backendLocal development, testing
d1Enable Cloudflare D1 backendProduction deployment

Note: Features are automatically enabled based on your target architecture. You typically don't need to specify them manually.

Project Structure

Here's a recommended project structure for d1-rs applications:

my-d1-project/
├── Cargo.toml
├── wrangler.toml              # Cloudflare Workers config
├── src/
│   ├── lib.rs                 # Main library
│   ├── models/                # Entity definitions
│   │   ├── mod.rs
│   │   ├── user.rs
│   │   └── post.rs
│   ├── migrations/            # Database migrations
│   │   ├── mod.rs
│   │   └── 001_create_users.rs
│   └── main.rs               # Worker entry point
├── tests/                    # Integration tests
│   ├── common/
│   │   └── mod.rs           # Test utilities  
│   ├── test_users.rs
│   └── test_posts.rs
└── README.md

Cloudflare Workers Setup

1. Install Wrangler

npm install -g wrangler
# or
pnpm install -g wrangler

2. Configure wrangler.toml

name = "my-d1-app"
main = "src/main.rs"
compatibility_date = "2024-01-01"

[build]
command = "cargo build --release --target wasm32-unknown-unknown"

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"

3. Create D1 Database

# Create database
wrangler d1 create my-database

# Apply migrations (once you've created them)
wrangler d1 migrations apply my-database

Local Development Setup

For local development and testing, d1-rs automatically uses SQLite:

1. Add Test Dependencies

[dev-dependencies]
rusqlite = "0.30"
tokio-test = "0.4"

2. Create Test Configuration

// tests/common/mod.rs
use d1_rs::*;

pub async fn setup_test_db() -> D1Client {
    D1Client::new_in_memory()
        .await
        .expect("Failed to create test database")
}

3. Run Tests

# Run all tests
cargo test

# Run with output
cargo test -- --nocapture

# Run specific test
cargo test test_user_creation

Verification

Verify your installation with this simple test:

// src/lib.rs
use d1_rs::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Entity)]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub name: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_basic_functionality() {
        let db = D1Client::new_in_memory().await.unwrap();
        
        let user = User::create()
            .set_name("Test User".to_string())
            .save(&db)
            .await
            .unwrap();

        assert_eq!(user.name, "Test User");
        println!("✅ d1-rs installation verified!");
    }
}

Run the test:

cargo test test_basic_functionality

If you see ✅ d1-rs installation verified!, you're all set!

Next Steps

Now that you have d1-rs installed, check out the Quick Start guide to build your first application!

🚀 Revolutionary Quick Start

Let's build a blog application using the world's most advanced ORM! This tutorial will showcase d1-rs's revolutionary compile-time safe relationships, nested eager loading, and impossible-to-match type safety.

🏆 What We'll Build

A revolutionary blog system demonstrating world-first capabilities:

  • Users who can write posts with compile-time safe relationships
  • Zero string literals - all relations type-safe at compile-time
  • World's first nested eager loading - prevent N+1 queries automatically
  • Impossible errors - typos become compile errors, not runtime crashes
  • Type-safe querying with perfect IDE auto-completion

Step 1: Define Your Entities

First, let's create our data models using d1-rs entities:

// src/models/mod.rs
pub mod user;
pub mod post;

pub use user::*;
pub use post::*;
// src/models/user.rs
use d1_rs::*;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
#[table(name = "users")]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub email: String,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
}
// src/models/post.rs
use d1_rs::*;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
#[table(name = "posts")]
pub struct Post {
    #[primary_key]
    pub id: i64,
    pub title: String,
    pub content: String,
    pub user_id: i64,  // Foreign key to users
    pub is_published: bool,
    pub created_at: DateTime<Utc>,
}

Step 2: Define Relations

Now let's define the relationships between our entities using d1-rs's type-safe relations system:

// src/models/relations.rs
use d1_rs::*;
use super::{User, Post};

// Define relations with the type-safe macro - NO STRING LITERALS!
relations! {
    User {
        has_many posts: Post via user_id,
    }
    
    Post {
        belongs_to user: User via user_id,
    }
}

Don't forget to include this in your models module:

// src/models/mod.rs
pub mod user;
pub mod post;
pub mod relations; // Add this line

pub use user::*;
pub use post::*;

Step 3: Create Database Schema

Now let's create a migration to set up our database schema:

// src/migrations/mod.rs
use d1_rs::*;

pub async fn run_migrations(db: &D1Client) -> Result<()> {
    // Create users table
    let users_migration = SchemaMigration::new("create_users".to_string())
        .create_table("users")
            .integer("id").primary_key().auto_increment().build()
            .text("name").not_null().build()
            .text("email").not_null().unique().build()
            .boolean("is_active").not_null().default_value(DefaultValue::Boolean(true)).build()
            .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .build();

    users_migration.execute(db).await?;

    // Create posts table
    let posts_migration = SchemaMigration::new("create_posts".to_string())
        .create_table("posts")
            .integer("id").primary_key().auto_increment().build()
            .text("title").not_null().build()
            .text("content").not_null().build()
            .integer("user_id").not_null().build()
            .boolean("is_published").not_null().default_value(DefaultValue::Boolean(false)).build()
            .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .build();

    posts_migration.execute(db).await?;

    Ok(())
}

Step 4: Using Type-Safe Relations

Now let's see the power of our type-safe relations system:

// src/lib.rs - Add these methods to the Blog impl
impl Blog {
    // Get a user's posts using type-safe association method - NO STRING LITERALS!
    pub async fn get_user_posts_typed(&self, user_id: i64) -> Result<Vec<Post>> {
        if let Some(user) = User::find(&self.db, user_id).await? {
            user.posts().all(&self.db).await  // Type-safe!
        } else {
            Ok(vec![])
        }
    }

    // Count user's posts without loading them
    pub async fn count_user_posts(&self, user_id: i64) -> Result<i64> {
        if let Some(user) = User::find(&self.db, user_id).await? {
            user.posts().count(&self.db).await  // Efficient counting
        } else {
            Ok(0)
        }
    }

    // Get post author using type-safe association
    pub async fn get_post_author(&self, post_id: i64) -> Result<Option<User>> {
        if let Some(post) = Post::find(&self.db, post_id).await? {
            post.user().first(&self.db).await  // Type-safe!
        } else {
            Ok(None)
        }
    }

    // Get user's first post 
    pub async fn get_user_first_post(&self, user_id: i64) -> Result<Option<Post>> {
        if let Some(user) = User::find(&self.db, user_id).await? {
            user.posts().first(&self.db).await  // Get first post only
        } else {
            Ok(None)
        }
    }
}

Step 5: Basic CRUD Operations

Let's implement some basic operations for our blog:

// src/lib.rs
pub mod models;
pub mod migrations;

use d1_rs::*;
use models::{User, Post};
use chrono::Utc;

pub struct Blog {
    db: D1Client,
}

impl Blog {
    pub async fn new() -> Result<Self> {
        let db = D1Client::new_in_memory().await?;
        migrations::run_migrations(&db).await?;
        Ok(Self { db })
    }

    // User operations
    pub async fn create_user(&self, name: String, email: String) -> Result<User> {
        User::create()
            .set_name(name)
            .set_email(email)
            .set_is_active(true)
            .set_created_at(Utc::now())
            .save(&self.db)
            .await
    }

    pub async fn get_user(&self, id: i64) -> Result<Option<User>> {
        User::find(&self.db, id).await
    }

    pub async fn list_active_users(&self) -> Result<Vec<User>> {
        User::query()
            .where_is_active_eq(true)
            .order_by_created_at_desc()
            .all(&self.db)
            .await
    }

    // Post operations
    pub async fn create_post(&self, user_id: i64, title: String, content: String) -> Result<Post> {
        Post::create()
            .set_title(title)
            .set_content(content)
            .set_user_id(user_id)
            .set_is_published(false)
            .set_created_at(Utc::now())
            .save(&self.db)
            .await
    }

    pub async fn publish_post(&self, post_id: i64) -> Result<Post> {
        Post::update(post_id)
            .set_is_published(true)
            .save(&self.db)
            .await
    }

    pub async fn get_published_posts(&self) -> Result<Vec<Post>> {
        Post::query()
            .where_is_published_eq(true)
            .order_by_created_at_desc()
            .all(&self.db)
            .await
    }

    pub async fn get_user_posts(&self, user_id: i64) -> Result<Vec<Post>> {
        Post::query()
            .where_user_id_eq(user_id)
            .order_by_created_at_desc()
            .all(&self.db)
            .await
    }
}

Step 6: 🏆 Revolutionary Nested Eager Loading

Now let's showcase d1-rs's world-first compile-time safe nested eager loading - impossible in any other ORM:

// Add these revolutionary methods to your Blog impl
impl Blog {
    // ✅ WORLD'S FIRST: Compile-time safe nested eager loading
    pub async fn get_users_with_posts_efficiently(&self) -> Result<Vec<UserWithPosts>> {
        // ✅ Single query with automatic JOINs - NO N+1 problems!
        User::query()
            .with_posts()  // ✅ Compile-time validated relation name!
            .all(&self.db)
            .await
    }
    
    // ✅ REVOLUTIONARY: Advanced nested eager loading with conditions
    pub async fn get_users_with_published_posts(&self) -> Result<Vec<UserWithPosts>> {
        // ✅ IMPOSSIBLE in other ORMs - nested conditions with compile-time safety!
        User::query()
            .with_posts(|posts| posts
                .where_is_published_eq(true)  // ✅ Condition in nested loading!
            )
            .all(&self.db)
            .await
    }
    
    // ✅ DEMONSTRATION: Compare old N+1 way vs revolutionary way
    pub async fn demonstrate_n1_prevention(&self) -> Result<()> {
        println!("❌ OLD WAY (N+1 problem in other ORMs):");
        let users = User::query().all(&self.db).await?;
        for user in users {
            // This would be N+1 queries in traditional approaches
            let _posts = user.posts().all(&self.db).await?;
        }
        
        println!("✅ REVOLUTIONARY WAY (single efficient query):");
        let _users_with_posts = User::query()
            .with_posts()  // ✅ Single query with automatic JOIN!
            .all(&self.db)
            .await?;
        
        println!("🚀 Result: ZERO N+1 queries, perfect performance!");
        Ok(())
    }
}

Step 7: Testing Your Revolutionary Application

Let's write some tests to make sure everything works:

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_blog_operations() {
        let blog = Blog::new().await.unwrap();

        // Create a user
        let user = blog.create_user(
            "Alice Smith".to_string(),
            "alice@example.com".to_string()
        ).await.unwrap();

        assert_eq!(user.name, "Alice Smith");
        assert_eq!(user.email, "alice@example.com");
        assert!(user.is_active);

        // Create a post
        let post = blog.create_post(
            user.id,
            "My First Post".to_string(),
            "This is the content of my first blog post!".to_string()
        ).await.unwrap();

        assert_eq!(post.title, "My First Post");
        assert_eq!(post.user_id, user.id);
        assert!(!post.is_published);  // Not published yet

        // Publish the post
        let published_post = blog.publish_post(post.id).await.unwrap();
        assert!(published_post.is_published);

        // Get published posts
        let published_posts = blog.get_published_posts().await.unwrap();
        assert_eq!(published_posts.len(), 1);
        assert_eq!(published_posts[0].id, post.id);

        // Get user's posts (traditional way)
        let user_posts = blog.get_user_posts(user.id).await.unwrap();
        assert_eq!(user_posts.len(), 1);
        assert_eq!(user_posts[0].id, post.id);

        // Test type-safe relations
        let typed_posts = blog.get_user_posts_typed(user.id).await.unwrap();
        assert_eq!(typed_posts.len(), 1);
        assert_eq!(typed_posts[0].id, post.id);

        // Test post count using association method
        let post_count = blog.count_user_posts(user.id).await.unwrap();
        assert_eq!(post_count, 1);

        // Test getting post author using type-safe association
        let author = blog.get_post_author(post.id).await.unwrap();
        assert!(author.is_some());
        assert_eq!(author.unwrap().id, user.id);
    }

    #[tokio::test]
    async fn test_user_queries() {
        let blog = Blog::new().await.unwrap();

        // Create multiple users
        let _user1 = blog.create_user("Alice".to_string(), "alice@example.com".to_string()).await.unwrap();
        let _user2 = blog.create_user("Bob".to_string(), "bob@example.com".to_string()).await.unwrap();

        // Test querying
        let users = blog.list_active_users().await.unwrap();
        assert_eq!(users.len(), 2);

        // Test finding by ID
        let user = blog.get_user(_user1.id).await.unwrap();
        assert!(user.is_some());
        assert_eq!(user.unwrap().name, "Alice");
    }
}

Step 7: Run Your Tests

cargo test

You should see output like:

running 2 tests
test tests::test_blog_operations ... ok
test tests::test_user_queries ... ok

test result: ok. 2 passed; 0 failed

🏆 Revolutionary Achievements Unlocked

In this quick start, you've experienced the world's most advanced ORM capabilities:

  1. Revolutionary Entity Definition: Zero-boilerplate #[derive(Entity)] with perfect type safety
  2. World's First Type-Safe Relations: relations! macro with zero string literals, compile-time validation
  3. Effortless Schema Migration: Automatic table creation with SchemaMigration
  4. Impossible-to-Match Association Methods: Generated methods with perfect IDE auto-completion
  5. Superior CRUD Operations: Fluent API that eliminates all runtime errors
  6. Compile-Time Safe Queries: Methods like where_is_active_eq() validated at compile-time
  7. World's First Nested Eager Loading: Automatic N+1 prevention with compile-time safety
  8. Perfect Testing: In-memory SQLite with zero configuration

You've just used ORM features that are IMPOSSIBLE in any other framework! 🌟

🚀 Next Steps - Explore the Revolution

Now that you've experienced the revolution, dive deeper into d1-rs's world-first capabilities:

Full Example

Here's the complete working example you can copy and run:

// Cargo.toml
[dependencies]
d1-rs = "0.1.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
chrono = { version = "0.4", features = ["serde"] }

[dev-dependencies]
rusqlite = "0.30"

Copy the code from steps 1-4 above into your src/lib.rs and run:

cargo test

Congratulations! 🎉 You've built your first d1-rs application!

Your First Entity

In this guide, we'll create your first d1-rs entity and understand how the derive macro works its magic to generate type-safe database operations.

What is an Entity?

An entity in d1-rs represents a database table as a Rust struct. The #[derive(Entity)] macro automatically generates:

  • Query builders with type-safe methods
  • Create and update builders
  • CRUD operations (Create, Read, Update, Delete)
  • Boolean field handling
  • Primary key management

Creating a Simple Entity

Let's start with a basic User entity:

use d1_rs::*;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

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

Understanding the Attributes

Required Derives

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
  • Entity: The d1-rs macro that generates database operations
  • Serialize, Deserialize: For JSON conversion (serde)
  • Debug, Clone, PartialEq: Standard Rust traits for debugging and comparisons

Field Annotations

#[primary_key]
pub id: i64,

The #[primary_key] attribute marks the primary key field. If no attribute is specified, d1-rs looks for an id field by default.

Custom Table Names

By default, d1-rs converts your struct name to snake_case and pluralizes it:

  • Userusers
  • BlogPostblog_posts

To specify a custom table name:

#[derive(Entity)]
#[table(name = "custom_users")]
pub struct User {
    // ...
}

Generated Functionality

When you derive Entity, d1-rs automatically generates several builders and methods:

Query Builder

// Generated automatically:
let users = User::query()
    .where_is_active_eq(true)           // Type-safe where clause
    .where_name_contains("Alice")       // String-specific methods
    .order_by_created_at_desc()         // Ordering
    .limit(10)                          // Pagination
    .all(&db)                           // Execute query
    .await?;

Create Builder

// Generated automatically:
let user = User::create()
    .set_name("Alice Smith".to_string())
    .set_email("alice@example.com".to_string())
    .set_is_active(true)
    .set_created_at(Utc::now())
    .save(&db)                          // Save to database
    .await?;

Update Builder

// Generated automatically:
let updated_user = User::update(user_id)
    .set_name("Alice Johnson".to_string())
    .set_is_active(false)
    .save(&db)
    .await?;

CRUD Methods

// Find by primary key
let user = User::find(&db, 42).await?;

// Delete by primary key  
User::delete(&db, 42).await?;

Field Types and Methods

d1-rs generates different query methods based on your field types:

String Fields

For String fields, you get:

.where_name_eq("Alice")           // Exact match
.where_name_ne("Bob")             // Not equal
.where_name_like("Al%")           // SQL LIKE
.where_name_contains("lic")       // Contains substring
.where_name_starts_with("Al")     // Starts with
.where_name_ends_with("ice")      // Ends with

Numeric Fields

For i64, i32, f64 etc., you get:

.where_id_eq(42)                  // Exact match
.where_id_gt(10)                  // Greater than
.where_id_gte(10)                 // Greater than or equal
.where_id_lt(100)                 // Less than
.where_id_lte(100)                // Less than or equal

Boolean Fields

For bool fields:

.where_is_active_eq(true)         // Boolean comparison
.where_is_active_ne(false)        // Boolean not equal

Note: d1-rs automatically handles boolean conversion between Rust's bool and SQLite's INTEGER (0/1) storage.

Creating the Database Table

Before using your entity, you need to create the corresponding database table:

use d1_rs::*;

async fn setup_database(db: &D1Client) -> Result<()> {
    let migration = SchemaMigration::new("create_users".to_string())
        .create_table("users")
            .integer("id").primary_key().auto_increment().build()
            .text("name").not_null().build()
            .text("email").not_null().unique().build()
            .boolean("is_active").not_null().default_value(DefaultValue::Boolean(true)).build()
            .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .build();

    migration.execute(db).await
}

Complete Example

Here's a complete working example:

use d1_rs::*;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

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

async fn example_usage() -> Result<()> {
    // Setup database (in-memory for testing)
    let db = D1Client::new_in_memory().await?;
    
    // Create table
    let migration = SchemaMigration::new("create_users".to_string())
        .create_table("users")
            .integer("id").primary_key().auto_increment().build()
            .text("name").not_null().build()
            .text("email").not_null().unique().build()
            .boolean("is_active").not_null().default_value(DefaultValue::Boolean(true)).build()
            .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .build();
    
    migration.execute(&db).await?;
    
    // Create a user
    let user = User::create()
        .set_name("Alice Smith".to_string())
        .set_email("alice@example.com".to_string())
        .set_is_active(true)
        .set_created_at(Utc::now())
        .save(&db)
        .await?;
    
    println!("Created user: {:?}", user);
    
    // Query users
    let active_users = User::query()
        .where_is_active_eq(true)
        .order_by_name_asc()
        .all(&db)
        .await?;
    
    println!("Active users: {}", active_users.len());
    
    // Update user
    let updated_user = User::update(user.id)
        .set_name("Alice Johnson".to_string())
        .save(&db)
        .await?;
    
    println!("Updated user: {:?}", updated_user);
    
    // Find by ID
    let found_user = User::find(&db, user.id).await?;
    println!("Found user: {:?}", found_user);
    
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_user_entity() {
        example_usage().await.unwrap();
    }
}

Common Patterns

Optional Fields

Use Option<T> for nullable database columns:

#[derive(Entity)]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub bio: Option<String>,  // Can be NULL in database
}

Custom Primary Keys

You can use different primary key types:

#[derive(Entity)]
pub struct Product {
    #[primary_key]
    pub sku: String,  // String primary key
    pub name: String,
    pub price: f64,
}

Timestamps

Common pattern for tracking creation and updates:

#[derive(Entity)]
pub struct Post {
    #[primary_key]
    pub id: i64,
    pub title: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: Option<DateTime<Utc>>,
}

Next Steps

Now that you understand entities, explore these advanced topics:

Entities

Entities are the heart of d1-rs - they represent your database tables as Rust structs with automatically generated type-safe operations.

Entity Basics

An entity is defined using the #[derive(Entity)] macro:

use d1_rs::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub email: String,
    pub is_active: bool,
}

Generated Builders

When you derive Entity, d1-rs automatically generates three builder types:

QueryBuilder

For reading data with type-safe where clauses:

let active_users = User::query()
    .where_is_active_eq(true)
    .where_name_starts_with("A")
    .order_by_created_at_desc()
    .limit(10)
    .all(&db)
    .await?;

CreateBuilder

For inserting new records:

let user = User::create()
    .set_name("Alice Smith".to_string())
    .set_email("alice@example.com".to_string())
    .set_is_active(true)
    .save(&db)
    .await?;

UpdateBuilder

For modifying existing records:

let updated_user = User::update(user_id)
    .set_name("Alice Johnson".to_string())
    .set_is_active(false)
    .save(&db)
    .await?;

Field Types & Query Methods

d1-rs generates different query methods based on your field types:

String Fields

pub name: String,

Generates methods:

  • .where_name_eq("exact") - Exact match
  • .where_name_ne("not_this") - Not equal
  • .where_name_like("pattern%") - SQL LIKE
  • .where_name_contains("substring") - Contains text
  • .where_name_starts_with("prefix") - Starts with
  • .where_name_ends_with("suffix") - Ends with

Numeric Fields

pub age: i32,
pub score: f64,

Generates methods:

  • .where_age_eq(25) - Exact match
  • .where_age_gt(18) - Greater than
  • .where_age_gte(21) - Greater than or equal
  • .where_age_lt(65) - Less than
  • .where_age_lte(30) - Less than or equal

Boolean Fields

pub is_active: bool,

Generates methods:

  • .where_is_active_eq(true) - Boolean comparison
  • .where_is_active_ne(false) - Boolean not equal

Important: Boolean fields are automatically converted between Rust's bool and SQLite's INTEGER (0/1) storage.

Optional Fields

pub middle_name: Option<String>,

Generates additional methods:

  • .where_middle_name_is_null() - Field is NULL
  • .where_middle_name_is_not_null() - Field is not NULL

Ordering and Pagination

Every field generates ordering methods:

User::query()
    .order_by_name_asc()        // Ascending order
    .order_by_created_at_desc() // Descending order
    .limit(50)                  // Limit results
    .offset(100)                // Skip results
    .all(&db)
    .await?;

Custom Table Names

By default, d1-rs converts struct names to snake_case and pluralizes them:

  • Userusers
  • BlogPostblog_posts
  • Categorycategories (correct pluralization)

Override with the table attribute:

#[derive(Entity)]
#[table(name = "people")]
pub struct User {
    // Uses "people" table instead of "users"
}

Primary Keys

Default Primary Key

If no #[primary_key] attribute is found, d1-rs looks for an id field:

#[derive(Entity)]
pub struct User {
    pub id: i64,  // Automatically treated as primary key
    pub name: String,
}

Custom Primary Key

Use #[primary_key] to specify a different field:

#[derive(Entity)]
pub struct Product {
    #[primary_key]
    pub sku: String,  // String primary key
    pub name: String,
    pub price: f64,
}

Composite Primary Keys

Currently not supported. Use a single primary key field.

CRUD Operations

Every entity gets these methods for free:

Find by Primary Key

// Returns Option<User>
let user = User::find(&db, 42).await?;

match user {
    Some(u) => println!("Found user: {}", u.name),
    None => println!("User not found"),
}

Delete by Primary Key

// Deletes the user with id = 42
User::delete(&db, 42).await?;

Exists Check

// Check if a user exists
let exists = User::query()
    .where_id_eq(42)
    .count(&db)
    .await? > 0;

Advanced Patterns

Timestamps

Common pattern for tracking creation and modification:

use chrono::{DateTime, Utc};

#[derive(Entity)]
pub struct Post {
    #[primary_key]
    pub id: i64,
    pub title: String,
    pub content: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: Option<DateTime<Utc>>,
}

// Usage
let post = Post::create()
    .set_title("My Post".to_string())
    .set_content("Content here".to_string())
    .set_created_at(Utc::now())
    .save(&db)
    .await?;

Soft Deletes

Implement soft deletes with a boolean field:

#[derive(Entity)]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub is_deleted: bool,
}

// Instead of User::delete(), update the flag
let soft_deleted = User::update(user_id)
    .set_is_deleted(true)
    .save(&db)
    .await?;

// Query only non-deleted users
let active_users = User::query()
    .where_is_deleted_eq(false)
    .all(&db)
    .await?;

Enums as Strings

Store enums as their string representation:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub enum UserRole {
    Admin,
    User,
    Guest,
}

#[derive(Entity)]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub name: String,
    // This will be stored as "Admin", "User", or "Guest" in the database
    pub role: UserRole,
}

Performance Tips

Indexing

Create database indexes for fields you query frequently:

// In your migration
let migration = SchemaMigration::new("add_indexes".to_string())
    .alter_table("users")
        .add_index("idx_users_email", vec!["email"])
        .add_unique_index("idx_users_username", vec!["username"])
    .build();

Batch Operations

For bulk inserts, use multiple create() calls in a transaction (when transactions are implemented):

// Current approach - individual inserts
for user_data in bulk_data {
    User::create()
        .set_name(user_data.name)
        .set_email(user_data.email)
        .save(&db)
        .await?;
}

Query Optimization

Use specific queries instead of loading all data:

// Good - only load what you need
let count = User::query()
    .where_is_active_eq(true)
    .count(&db)
    .await?;

// Less efficient - loads all data
let users = User::query().all(&db).await?;
let active_count = users.iter().filter(|u| u.is_active).count();

Error Handling

Entity operations can fail in several ways:

use d1_rs::{D1RsError, Result};

async fn handle_user_creation(db: &D1Client) -> Result<()> {
    let result = User::create()
        .set_email("invalid-email")  // This might cause validation errors
        .save(db)
        .await;
    
    match result {
        Ok(user) => {
            println!("User created: {}", user.id);
            Ok(())
        }
        Err(D1RsError::Database(msg)) => {
            eprintln!("Database error: {}", msg);
            Err(D1RsError::Database(msg))
        }
        Err(D1RsError::SerializationError(msg)) => {
            eprintln!("Serialization error: {}", msg);
            Err(D1RsError::SerializationError(msg))
        }
        Err(other) => Err(other),
    }
}

Next Steps

  • Learn about Queries for advanced querying techniques
  • Explore Relations to connect entities together
  • Check out Boolean Handling for deep understanding of boolean conversion

Queries

d1-rs provides a powerful, type-safe query builder that generates SQL at compile time. Every entity automatically gets rich querying capabilities based on its field types.

Basic Querying

Simple Queries

// Get all users
let users = User::query().all(&db).await?;

// Get first user
let user = User::query().first(&db).await?;

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

Where Clauses

Every field generates type-appropriate where methods:

let results = User::query()
    .where_name_eq("Alice")           // Exact match
    .where_age_gte(18)                // Greater than or equal
    .where_is_active_eq(true)         // Boolean comparison
    .where_email_contains("@gmail")   // String contains
    .all(&db)
    .await?;

Field-Specific Methods

String Fields

User::query()
    .where_name_eq("Alice")              // name = 'Alice'
    .where_name_ne("Bob")                // name != 'Bob'  
    .where_name_like("Al%")              // name LIKE 'Al%'
    .where_name_contains("lic")          // name LIKE '%lic%'
    .where_name_starts_with("Al")        // name LIKE 'Al%'
    .where_name_ends_with("ice")         // name LIKE '%ice'
    .all(&db).await?;

Numeric Fields

User::query()
    .where_age_eq(25)                    // age = 25
    .where_age_ne(30)                    // age != 30
    .where_age_gt(18)                    // age > 18
    .where_age_gte(21)                   // age >= 21
    .where_age_lt(65)                    // age < 65  
    .where_age_lte(64)                   // age <= 64
    .all(&db).await?;

Boolean Fields

User::query()
    .where_is_active_eq(true)            // is_active = 1
    .where_is_active_ne(false)           // is_active != 0
    .all(&db).await?;

Optional/Nullable Fields

User::query()
    .where_middle_name_is_null()         // middle_name IS NULL
    .where_middle_name_is_not_null()     // middle_name IS NOT NULL
    .all(&db).await?;

Combining Conditions

Multiple Where Clauses

All where clauses are combined with AND:

let results = User::query()
    .where_is_active_eq(true)           // AND is_active = 1
    .where_age_gte(18)                  // AND age >= 18
    .where_name_contains("john")        // AND name LIKE '%john%'
    .all(&db)
    .await?;

// Generates: WHERE is_active = 1 AND age >= 18 AND name LIKE '%john%'

IN Queries (Limited)

Basic IN support (currently uses first value):

let user_ids = vec![1, 2, 3, 4, 5];
let users = User::query()
    .where_id_in(user_ids)              // Limited implementation
    .all(&db)
    .await?;

Note: Full IN query support is planned for future releases.

Ordering

Every field generates ordering methods:

User::query()
    .order_by_name_asc()                // ORDER BY name ASC
    .order_by_created_at_desc()         // ORDER BY created_at DESC
    .all(&db).await?;

// Multiple ordering
User::query()
    .order_by_is_active_desc()          // ORDER BY is_active DESC,
    .order_by_name_asc()                //         name ASC
    .all(&db).await?;

Generic Ordering

Use the generic order_by method for dynamic ordering:

let order_column = "name";
let ascending = true;

User::query()
    .order_by(order_column, if ascending { "ASC" } else { "DESC" })
    .all(&db).await?;

Pagination

Limit and Offset

// Get first 10 users
let first_page = User::query()
    .limit(10)
    .all(&db).await?;

// Get next 10 users (skip first 10)
let second_page = User::query()
    .limit(10)
    .offset(10)
    .all(&db).await?;

// Pagination helper function
async fn get_users_page(db: &D1Client, page: i64, per_page: i64) -> Result<Vec<User>> {
    User::query()
        .order_by_id_asc()              // Consistent ordering for pagination
        .limit(per_page)
        .offset(page * per_page)
        .all(db)
        .await
}

Query Results

Different Result Types

// All matching records
let users: Vec<User> = User::query()
    .where_is_active_eq(true)
    .all(&db).await?;

// First matching record
let user: Option<User> = User::query()
    .where_email_eq("alice@example.com")
    .first(&db).await?;

// Count of matching records
let count: i64 = User::query()
    .where_is_active_eq(true)
    .count(&db).await?;

Handling Results

// Handle optional results
match User::query().where_id_eq(42).first(&db).await? {
    Some(user) => println!("Found user: {}", user.name),
    None => println!("User not found"),
}

// Check if any records exist
let has_active_users = User::query()
    .where_is_active_eq(true)
    .count(&db).await? > 0;

// Get first or fail
let user = User::query()
    .where_email_eq("admin@example.com")
    .first(&db).await?
    .ok_or(D1RsError::NotFound)?;

Performance Considerations

Query Optimization

// Good - specific query
let active_count = User::query()
    .where_is_active_eq(true)
    .count(&db).await?;

// Less efficient - loads all data
let all_users = User::query().all(&db).await?;
let active_count = all_users.iter().filter(|u| u.is_active).count();

Indexing Strategy

Create indexes for frequently queried fields:

// In your migration
let migration = SchemaMigration::new("add_indexes".to_string())
    .alter_table("users")
        .add_index("idx_users_email", vec!["email"])
        .add_index("idx_users_active", vec!["is_active"])
        .add_index("idx_users_created", vec!["created_at"])
    .build();

Limit Large Queries

Always use limits for potentially large result sets:

// Good - limited results
let recent_posts = Post::query()
    .order_by_created_at_desc()
    .limit(100)
    .all(&db).await?;

// Potentially dangerous - could return millions of records
let all_posts = Post::query().all(&db).await?;

Complex Query Patterns

Search Functionality

async fn search_users(db: &D1Client, search_term: &str) -> Result<Vec<User>> {
    let term = format!("%{}%", search_term.to_lowercase());
    
    User::query()
        .where_name_like(&term)
        .order_by_name_asc()
        .limit(50)
        .all(db)
        .await
}

Date Range Queries

use chrono::{DateTime, Utc, Duration};

async fn get_recent_posts(db: &D1Client, days: i64) -> Result<Vec<Post>> {
    let cutoff = Utc::now() - Duration::days(days);
    
    Post::query()
        .where_created_at_gte(cutoff)
        .where_is_published_eq(true)
        .order_by_created_at_desc()
        .all(db)
        .await
}

Active Record Pattern

impl User {
    // Custom query methods on your entity
    pub async fn find_by_email(db: &D1Client, email: &str) -> Result<Option<User>> {
        User::query()
            .where_email_eq(email)
            .first(db)
            .await
    }
    
    pub async fn active_users(db: &D1Client) -> Result<Vec<User>> {
        User::query()
            .where_is_active_eq(true)
            .order_by_name_asc()
            .all(db)
            .await
    }
    
    pub async fn users_by_domain(db: &D1Client, domain: &str) -> Result<Vec<User>> {
        let pattern = format!("%@{}", domain);
        User::query()
            .where_email_like(&pattern)
            .all(db)
            .await
    }
}

// Usage
let user = User::find_by_email(&db, "alice@example.com").await?;
let active_users = User::active_users(&db).await?;
let gmail_users = User::users_by_domain(&db, "gmail.com").await?;

Error Handling

use d1_rs::{D1RsError, Result};

async fn safe_user_query(db: &D1Client, user_id: i64) -> Result<User> {
    match User::find(db, user_id).await {
        Ok(Some(user)) => Ok(user),
        Ok(None) => Err(D1RsError::NotFound),
        Err(D1RsError::Database(msg)) => {
            eprintln!("Database error: {}", msg);
            Err(D1RsError::Database(msg))
        }
        Err(other) => Err(other),
    }
}

Raw SQL (Future)

For complex queries not supported by the query builder:

// This will be available in future versions
let users = db.query_raw::<User>(
    "SELECT * FROM users WHERE LOWER(name) = LOWER(?) AND created_at > ?",
    &[
        serde_json::Value::String("Alice".to_string()),
        serde_json::Value::String(yesterday.to_rfc3339()),
    ]
).await?;

Next Steps

Migrations

d1-rs provides a powerful schema migration system that lets you evolve your database schema over time with type-safe operations and full ALTER TABLE support.

Schema Migrations

Creating Your First Migration

use d1_rs::*;

async fn create_users_table(db: &D1Client) -> Result<()> {
    let migration = SchemaMigration::new("create_users".to_string())
        .create_table("users")
            .integer("id").primary_key().auto_increment().build()
            .text("name").not_null().build()
            .text("email").not_null().unique().build()
            .boolean("is_active").not_null().default_value(DefaultValue::Boolean(true)).build()
            .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .build();
    
    migration.execute(db).await
}

Migration Structure

Migrations use a fluent builder API:

SchemaMigration::new("migration_name".to_string())
    .create_table("table_name")
        // Column definitions
    .build()
    .alter_table("existing_table") 
        // Modifications
    .build()
    .raw_sql("CREATE INDEX ...")
    .execute(&db).await

Column Types

d1-rs supports all standard SQLite column types:

Basic Types

.create_table("products")
    .integer("id").primary_key().auto_increment().build()
    .text("name").not_null().build()
    .real("price").not_null().build()
    .boolean("in_stock").default_value(DefaultValue::Boolean(true)).build()
    .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
    .json("metadata").build()
.build()

Column Types Reference

d1-rs TypeSQLite TypeRust TypeDescription
integer()INTEGERi6464-bit signed integer
text()TEXTStringUTF-8 string
real()REALf64Floating point number
boolean()INTEGERboolBoolean (stored as 0/1)
datetime()DATETIMEDateTime<Utc>ISO 8601 datetime
json()TEXTserde_json::ValueJSON data

Column Constraints

Primary Keys

.integer("id").primary_key().auto_increment().build()
.text("sku").primary_key().build()  // String primary key

Constraints

.text("email")
    .not_null()                                    // NOT NULL
    .unique()                                      // UNIQUE
    .default_value(DefaultValue::Text("".to_string()))  // DEFAULT
    .build()

.integer("age")
    .check("age >= 0 AND age <= 150")             // CHECK constraint
    .build()

Default Values

// Type-safe default values
.boolean("is_active").default_value(DefaultValue::Boolean(true)).build()
.integer("count").default_value(DefaultValue::Integer(0)).build()
.text("status").default_value(DefaultValue::Text("pending".to_string())).build()
.datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
.real("score").default_value(DefaultValue::Real(0.0)).build()

// Raw SQL expressions
.datetime("updated_at").default_value(DefaultValue::Expression("CURRENT_TIMESTAMP".to_string())).build()

Foreign Keys

Basic Foreign Keys

.create_table("posts")
    .integer("id").primary_key().auto_increment().build()
    .integer("user_id")
        .not_null()
        .references("users", "id")  // Foreign key reference
        .build()
    .text("title").not_null().build()
.build()

Foreign Key Actions

use d1_rs::schema::ForeignKeyAction;

.integer("user_id")
    .references("users", "id")
    .on_delete(ForeignKeyAction::Cascade)     // ON DELETE CASCADE
    .on_update(ForeignKeyAction::SetNull)     // ON UPDATE SET NULL
    .build()

Available actions:

  • Cascade - Delete/update cascades to child records
  • SetNull - Set foreign key to NULL
  • SetDefault - Set foreign key to default value
  • Restrict - Prevent the action if child records exist
  • NoAction - No action (database default)

Schema Evolution

Adding Columns

let migration = SchemaMigration::new("add_user_phone".to_string())
    .alter_table("users")
        .add_column("phone", ColumnType::Text)
            .build()
    .build();

migration.execute(&db).await?;

Adding Indexes

let migration = SchemaMigration::new("add_indexes".to_string())
    .alter_table("users")
        .add_index("idx_users_email", vec!["email"])
        .add_unique_index("idx_users_username", vec!["username"]) 
        .add_index("idx_users_multi", vec!["is_active", "created_at"])
    .build();

Renaming Columns

let migration = SchemaMigration::new("rename_column".to_string())
    .alter_table("users")
        .rename_column("old_name", "new_name")
    .build();

Relations in Migrations

Define relationships between tables:

let migration = SchemaMigration::new("create_blog_schema".to_string())
    // Create tables first
    .create_table("users")
        .integer("id").primary_key().auto_increment().build()
        .text("name").not_null().build()
    .build()
    
    .create_table("posts")
        .integer("id").primary_key().auto_increment().build()
        .integer("user_id").not_null().build()
        .text("title").not_null().build()
    .build()
    
    // Then define relations
    .create_relation("user_posts", "users", "posts")
        .one_to_many("user_id", "id")
    .build();

migration.execute(&db).await?;

Relation Types

// One-to-One: User has one Profile
.create_relation("user_profile", "users", "profiles")
    .one_to_one("user_id", "id")
.build()

// One-to-Many: User has many Posts  
.create_relation("user_posts", "users", "posts")
    .one_to_many("user_id", "id")
.build()

// Many-to-Many: Posts have many Categories
.create_relation("post_categories", "posts", "categories")
    .many_to_many("post_categories", "post_id", "id", "category_id", "id")
.build()

Advanced Migrations

Raw SQL

For complex operations not supported by the builder:

let migration = SchemaMigration::new("complex_operation".to_string())
    .raw_sql("CREATE INDEX IF NOT EXISTS idx_users_fulltext ON users USING fts5(name, email)")
    .raw_sql("CREATE TRIGGER update_modified_time AFTER UPDATE ON users BEGIN UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; END")
    .execute(&db).await?;

Multiple Operations

Chain multiple operations in a single migration:

let migration = SchemaMigration::new("blog_setup".to_string())
    .create_table("categories")
        .integer("id").primary_key().auto_increment().build()
        .text("name").not_null().unique().build()
    .build()
    
    .create_table("posts")
        .integer("id").primary_key().auto_increment().build()
        .text("title").not_null().build()
        .text("content").not_null().build()
        .integer("category_id").references("categories", "id").build()
    .build()
    
    .alter_table("posts")
        .add_index("idx_posts_category", vec!["category_id"])
        .add_index("idx_posts_title", vec!["title"])
    .build();

migration.execute(&db).await?;

Migration Management

Organizing Migrations

Create a migration runner for your application:

pub struct MigrationRunner;

impl MigrationRunner {
    pub async fn run_all(db: &D1Client) -> Result<()> {
        // Run migrations in order
        Self::create_users_table(db).await?;
        Self::create_posts_table(db).await?;
        Self::add_user_indexes(db).await?;
        Self::create_relations(db).await?;
        Ok(())
    }
    
    async fn create_users_table(db: &D1Client) -> Result<()> {
        let migration = SchemaMigration::new("001_create_users".to_string())
            .create_table("users")
                .integer("id").primary_key().auto_increment().build()
                .text("name").not_null().build()
                .text("email").not_null().unique().build()
                .boolean("is_active").default_value(DefaultValue::Boolean(true)).build()
                .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
            .build();
        
        migration.execute(db).await
    }
    
    async fn create_posts_table(db: &D1Client) -> Result<()> {
        let migration = SchemaMigration::new("002_create_posts".to_string())
            .create_table("posts")
                .integer("id").primary_key().auto_increment().build()
                .integer("user_id").not_null().references("users", "id").build()
                .text("title").not_null().build()
                .text("content").not_null().build()
                .boolean("is_published").default_value(DefaultValue::Boolean(false)).build()
                .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
            .build();
        
        migration.execute(db).await
    }
    
    // ... more migrations
}

// Usage
#[tokio::main]
async fn main() -> Result<()> {
    let db = D1Client::new_in_memory().await?;
    MigrationRunner::run_all(&db).await?;
    Ok(())
}

Environment-Specific Migrations

pub async fn setup_database(db: &D1Client, env: Environment) -> Result<()> {
    // Always run core migrations
    MigrationRunner::run_core_migrations(db).await?;
    
    match env {
        Environment::Test => {
            MigrationRunner::run_test_data(db).await?;
        }
        Environment::Development => {
            MigrationRunner::run_dev_data(db).await?;
        }
        Environment::Production => {
            // Production-only migrations
        }
    }
    
    Ok(())
}

Testing Migrations

Migration Tests

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_user_migration() {
        let db = D1Client::new_in_memory().await.unwrap();
        
        // Run migration
        MigrationRunner::create_users_table(&db).await.unwrap();
        
        // Test that table was created correctly
        let user = User::create()
            .set_name("Test User".to_string())
            .set_email("test@example.com".to_string())
            .save(&db)
            .await
            .unwrap();
        
        assert_eq!(user.name, "Test User");
        assert!(user.is_active); // Should default to true
    }
    
    #[tokio::test]
    async fn test_full_schema() {
        let db = D1Client::new_in_memory().await.unwrap();
        
        // Run all migrations
        MigrationRunner::run_all(&db).await.unwrap();
        
        // Test that all entities work
        let user = User::create()
            .set_name("Author".to_string())
            .set_email("author@example.com".to_string())
            .save(&db)
            .await
            .unwrap();
        
        let post = Post::create()
            .set_user_id(user.id)
            .set_title("Test Post".to_string())
            .set_content("Content here".to_string())
            .save(&db)
            .await
            .unwrap();
        
        assert_eq!(post.user_id, user.id);
    }
}

Best Practices

Migration Naming

Use descriptive, sequential names:

"001_create_users_table"
"002_create_posts_table"
"003_add_user_phone_column"
"004_create_categories_table"
"005_add_post_category_relation"

Safe Schema Changes

Always use safe operations in production:

// Safe - adds optional column
.alter_table("users")
    .add_column("phone", ColumnType::Text)  // NULL allowed
    .build()

// Potentially unsafe - adds required column to existing table
.alter_table("users") 
    .add_column("required_field", ColumnType::Text)
        .not_null()  // This could fail if table has existing rows
        .build()

Rollback Strategy

Plan for rollbacks by keeping operations reversible when possible:

// Instead of dropping columns, consider marking as unused
.alter_table("users")
    .add_column("old_column_unused", ColumnType::Boolean)
        .default_value(DefaultValue::Boolean(false))
        .build()

// Rather than renaming, add new column and migrate data gradually

Next Steps

  • Learn about Schema Evolution for advanced schema management
  • Explore Relations for connecting tables with foreign keys
  • Check out Testing for comprehensive migration testing strategies

Schema Evolution

🚀 Revolutionary Relations System

d1-rs provides the world's most advanced, completely type-safe relations system that EXCEEDS every ORM in existence. We are the first ORM in any language to provide compile-time safe nested eager loading, recursive relationships, and rich M2M junction entities.

🏆 Revolutionary Relation Types - World Firsts

One-to-One Relations with Compile-Time Safety

User 1 ←→ 1 Profile  // ✅ All access compile-time validated

One-to-Many Relations with Advanced Eager Loading

User 1 ←→ ∞ Post     // ✅ Nested eager loading: .with_posts(|p| p.with_categories())

Many-to-Many Relations with Rich Junction Entities

Post ∞ ←→ ∞ Category  // ✅ Junction tables as first-class entities with full CRUD

🔥 World's First: Recursive Self-Referential Relations

Category ∞ ←→ ∞ Category  // ✅ parent/children with compile-time safety
User 1 ←→ ∞ User          // ✅ manager/employees with automatic null handling
Comment 1 ←→ ∞ Comment    // ✅ parent_comment/replies with unlimited depth

🚀 Revolutionary Key Features - Unmatched by Any ORM

🏆 World's First: Compile-Time Safe Nested Eager Loading

// ✅ IMPOSSIBLE in any other ORM - Compile-time validated nested relations!
let users_with_data = User::query()
    .with_posts(|posts| posts.with_categories())  // ✅ Nested with validation!
    .with_profile()
    .all(&db).await?;

// ✅ REVOLUTIONARY: Unlimited nesting depth
let complex_data = User::query()
    .with_posts(|posts| posts
        .with_categories(|categories| categories
            .with_parent()  // ✅ Recursive relations in nested loading!
        )
        .with_comments(|comments| comments
            .with_replies()  // ✅ Recursive comments in nested loading!
        )
    )
    .all(&db).await?;

🔥 World's First: Recursive Relationships with Type Safety

// ✅ Category hierarchy with compile-time safety
let root_children = root_category.children().all(&db).await?;
let parent_category = child.parent().first(&db).await?;

// ✅ User management hierarchy  
let manager = employee.manager().first(&db).await?;
let team_members = manager.employees().all(&db).await?;

// ✅ Comment reply threads
let comment_replies = comment.replies().all(&db).await?;
let parent_comment = reply.parent_comment().first(&db).await?;

// This would cause a compile error - method doesn't exist
let invalid = user.invalid_relation(); // ❌ Compile-time error

🚀 World's First: Rich M2M Junction Entities

// ✅ Junction entities as first-class citizens with full Entity powers!
#[derive(Entity)]
struct UserRole {
    id: i64,
    user_id: i64, 
    role_id: i64,
    granted_at: DateTime<Utc>,     // ✅ Rich additional data!
    granted_by: String,            // ✅ Who granted the role?
    expires_at: Option<DateTime<Utc>>, // ✅ Role expiration?
    is_active: bool,              // ✅ Role status?
}

// ✅ Query junction entities directly - IMPOSSIBLE in other ORMs!
let active_roles = UserRole::query()
    .where_is_active_eq(true)                    // ✅ Type-safe boolean!
    .where_expires_at_gt(current_timestamp)      // ✅ Type-safe timestamp!
    .all(&db).await?;

// ✅ Navigate through junction entities
let user_from_junction = user_role.user().first(&db).await?;
let role_from_junction = user_role.role().first(&db).await?;

🎯 Zero String Literals with Perfect Type Safety

// Type-safe association methods - NO STRING LITERALS ANYWHERE!
let user_posts = user.posts().all(&db).await?;
let post_count = user.posts().count(&db).await?;
let first_post = user.posts().first(&db).await?;

// Many-to-one: Post belongs to User  
let post_author = post.user().first(&db).await?;

// Many-to-many through junction entities
let post_categories = post.post_categories().all(&db).await?;

Migration Auto-Generation

Junction tables and relations are created automatically from your entity definitions:

let migration = SchemaMigration::new("blog".to_string())
    // Create your entity tables normally...
    .auto_generate_for::<User>()     // Generates all User relations
    .auto_generate_for::<Post>()     // Generates all Post relations  
    .auto_generate_for::<Category>(); // Generates all Category relations

Setting Up Relations

Relations are defined using the simple relations! macro - no complex migration code needed:

use d1_rs::*;

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct User {
    #[primary_key] pub id: i64,
    pub name: String,
    pub email: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct Post {
    #[primary_key] pub id: i64,
    pub user_id: i64,
    pub title: String,
    pub content: String,
}

// Define relations with one simple macro - NO STRING LITERALS!
relations! {
    User {
        has_many posts: Post via user_id,
    }
    
    Post {
        belongs_to user: User via user_id,
    }
}

// That's it! Now you have type-safe association methods
let user_posts = user.posts().all(&db).await?;
let post_author = post.user().first(&db).await?;

Working with Relations

Association Methods

The relations! macro generates type-safe methods for each relation:

// Get all related records
let all_posts = user.posts().all(&db).await?;

// Count related records
let post_count = user.posts().count(&db).await?;

// Get first related record
let latest_post = user.posts().first(&db).await?;

// Many-to-one relations
let post_author = post.user().first(&db).await?;

// Many-to-many relations  
let post_categories = post.categories().all(&db).await?;

Loaded Data Access

If relations are pre-loaded, access them without database queries:

// Check if data is already loaded
if let Some(posts) = user.posts().loaded() {
    println!("Already have {} posts loaded", posts.len());
} else {
    // This will query the database
    let posts = user.posts().all(&db).await?;
}

Many-to-Many Relations

For many-to-many relations, simply define them in the macro and let d1-rs handle everything:

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
#[table(name = "post_categories")]  // Specify junction table name
pub struct PostCategory {
    #[primary_key] pub id: i64,
    pub post_id: i64,
    pub category_id: i64,
}

relations! {
    Post {
        has_many_through categories: Category via post_id,
    }
    
    Category {
        has_many_through posts: Post via category_id,
    }
}

// Junction table is created automatically with migration auto-generation
let migration = SchemaMigration::new("blog".to_string())
    .auto_generate_for::<Post>()
    .auto_generate_for::<Category>();

Managing Relations

Attach/Detach (Many-to-Many)

// Attach a category to a post
post.categories().attach(&db, category.id).await?;

// Detach a category from a post
post.categories().detach(&db, category.id).await?;

Performance Considerations

Lazy Loading

Each association method call executes a separate query:

// This executes one query per user - N+1 problem
for user in users {
    let posts = user.posts().all(&db).await?; 
}

Future versions will include eager loading capabilities to solve N+1 queries efficiently.

Next Steps

Ready to implement relations in your application? Check out specific guides:

The new type-safe API eliminates the need for string literals and provides compile-time safety that wasn't possible with the old traversal system.

One-to-One Relations

One-to-one relations represent a direct 1:1 relationship between two entities, where each record in one table corresponds to exactly one record in another table.

Basic One-to-One

Schema Definition

// User has one Profile
let migration = SchemaMigration::new("create_user_profile_relation".to_string())
    .create_table("users")
        .integer("id").primary_key().auto_increment().build()
        .text("name").not_null().build()
        .text("email").not_null().unique().build()
    .build()
    
    .create_table("profiles")
        .integer("id").primary_key().auto_increment().build()
        .integer("user_id").not_null().unique().build() // UNIQUE constraint ensures 1:1
        .text("bio").build()
        .text("avatar_url").build()
        .datetime("updated_at").default_value(DefaultValue::CurrentTimestamp).build()
    .build()
    
    // Define the relation
    .create_relation("user_profile", "users", "profiles")
        .one_to_one("user_id", "id")
    .build();

migration.execute(&db).await?;

Entity Definitions

use d1_rs::*;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone, Entity, RelationalEntity, PartialEq)]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub email: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, Entity, RelationalEntity, PartialEq)]
pub struct Profile {
    #[primary_key]
    pub id: i64,
    pub user_id: i64,
    pub bio: Option<String>,
    pub avatar_url: Option<String>,
    pub updated_at: DateTime<Utc>,
}

Traversing One-to-One Relations

From Parent to Child

// Get user's profile
let user = User::find(&db, 1).await?.unwrap();
let profile = user.traverse::<Profile>(&db, "profile").await?;

match profile.first() {
    Some(profile) => println!("User bio: {:?}", profile.bio),
    None => println!("User has no profile"),
}

From Child to Parent

// Get profile's user
let profile = Profile::find(&db, 1).await?.unwrap();
let users = profile.traverse::<User>(&db, "user").await?;

if let Some(user) = users.first() {
    println!("Profile belongs to: {}", user.name);
}

Eager Loading

Load users with their profiles in a single operation:

// Load users with profiles
let users_with_profiles = User::query()
    .with(vec!["profile"])
    .all(&db)
    .await?;

for user in users_with_profiles {
    println!("User: {}", user.name);
    
    // Access loaded profile data
    let profiles = user.traverse::<Profile>(&db, "profile").await?;
    if let Some(profile) = profiles.first() {
        println!("  Bio: {:?}", profile.bio);
    }
}

Creating User with Profile

// Create user first
let user = User::create()
    .set_name("Alice Johnson".to_string())
    .set_email("alice@example.com".to_string())
    .save(&db)
    .await?;

// Create associated profile
let profile = Profile::create()
    .set_user_id(user.id)
    .set_bio(Some("Software engineer and coffee enthusiast".to_string()))
    .set_avatar_url(Some("https://example.com/avatar.jpg".to_string()))
    .set_updated_at(Utc::now())
    .save(&db)
    .await?;

println!("Created user {} with profile {}", user.id, profile.id);

Advanced Patterns

Profile with Required Relationship

Ensure every profile has a valid user:

impl Profile {
    pub async fn create_for_user(
        db: &D1Client,
        user_id: i64,
        bio: Option<String>,
        avatar_url: Option<String>,
    ) -> Result<Profile> {
        // Verify user exists
        let user = User::find(db, user_id).await?
            .ok_or(D1RsError::NotFound)?;
        
        Profile::create()
            .set_user_id(user.id)
            .set_bio(bio)
            .set_avatar_url(avatar_url)
            .set_updated_at(Utc::now())
            .save(db)
            .await
    }
    
    pub async fn get_user(&self, db: &D1Client) -> Result<User> {
        User::find(db, self.user_id).await?
            .ok_or(D1RsError::NotFound)
    }
}

User with Profile Helper

impl User {
    pub async fn get_profile(&self, db: &D1Client) -> Result<Option<Profile>> {
        Profile::query()
            .where_user_id_eq(self.id)
            .first(db)
            .await
    }
    
    pub async fn create_profile(
        &self,
        db: &D1Client,
        bio: Option<String>,
        avatar_url: Option<String>,
    ) -> Result<Profile> {
        // Check if profile already exists
        if let Some(_) = self.get_profile(db).await? {
            return Err(D1RsError::Database("User already has a profile".to_string()));
        }
        
        Profile::create()
            .set_user_id(self.id)
            .set_bio(bio)
            .set_avatar_url(avatar_url)
            .set_updated_at(Utc::now())
            .save(db)
            .await
    }
}

Optional vs Required Relations

Optional Profile (Current Implementation)

// User may or may not have a profile
let user = User::find(&db, 1).await?.unwrap();
match user.get_profile(&db).await? {
    Some(profile) => println!("Has profile: {:?}", profile.bio),
    None => println!("No profile found"),
}

Required Profile Pattern

For cases where the relationship should always exist:

#[derive(Entity, RelationalEntity)]
pub struct Account {
    #[primary_key]
    pub id: i64,
    pub username: String,
    // profile_id as required foreign key
    pub profile_id: i64,
}

#[derive(Entity, RelationalEntity)]
pub struct AccountProfile {
    #[primary_key]
    pub id: i64,
    pub display_name: String,
    pub settings: String, // JSON settings
}

impl Account {
    pub async fn create_with_profile(
        db: &D1Client,
        username: String,
        display_name: String,
    ) -> Result<(Account, AccountProfile)> {
        // Create profile first
        let profile = AccountProfile::create()
            .set_display_name(display_name)
            .set_settings("{}".to_string())
            .save(db)
            .await?;
        
        // Create account with profile reference
        let account = Account::create()
            .set_username(username)
            .set_profile_id(profile.id)
            .save(db)
            .await?;
        
        Ok((account, profile))
    }
}

Performance Considerations

Indexing Foreign Keys

Always index foreign key columns for better performance:

let migration = SchemaMigration::new("add_profile_indexes".to_string())
    .alter_table("profiles")
        .add_unique_index("idx_profiles_user_id", vec!["user_id"])
    .build();

Selective Loading

Only load profiles when needed:

// Good - only load when you need the profile data
let users = User::query()
    .where_is_active_eq(true)
    .all(&db)
    .await?;

for user in users {
    if should_show_profile(&user) {
        let profile = user.get_profile(&db).await?;
        // Use profile data
    }
}

// Less efficient - always loads profile data
let users_with_profiles = User::query()
    .with(vec!["profile"])
    .all(&db)
    .await?;

Common Patterns

User Settings

#[derive(Entity, RelationalEntity)]
pub struct UserSettings {
    #[primary_key]
    pub id: i64,
    pub user_id: i64,
    pub theme: String,
    pub notifications_enabled: bool,
    pub language: String,
}

impl User {
    pub async fn get_or_create_settings(&self, db: &D1Client) -> Result<UserSettings> {
        if let Some(settings) = UserSettings::query()
            .where_user_id_eq(self.id)
            .first(db)
            .await? 
        {
            Ok(settings)
        } else {
            // Create default settings
            UserSettings::create()
                .set_user_id(self.id)
                .set_theme("light".to_string())
                .set_notifications_enabled(true)
                .set_language("en".to_string())
                .save(db)
                .await
        }
    }
}

Profile Updates

impl Profile {
    pub async fn update_bio(&self, db: &D1Client, new_bio: String) -> Result<Profile> {
        Profile::update(self.id)
            .set_bio(Some(new_bio))
            .set_updated_at(Utc::now())
            .save(db)
            .await
    }
    
    pub async fn update_avatar(&self, db: &D1Client, avatar_url: String) -> Result<Profile> {
        Profile::update(self.id)
            .set_avatar_url(Some(avatar_url))
            .set_updated_at(Utc::now())
            .save(db)
            .await
    }
}

Next Steps

One-to-Many Relations

One-to-many relations represent relationships where one record can be associated with multiple records in another table. With d1-rs's new type-safe API, these relationships are completely string-literal-free and validated at compile time.

Basic One-to-Many Setup

Entity Definitions

use d1_rs::*;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub email: String,
    pub is_active: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct Post {
    #[primary_key]
    pub id: i64,
    pub user_id: i64, // Foreign key to users
    pub title: String,
    pub content: String,
    pub is_published: bool,
    pub created_at: DateTime<Utc>,
}

Relations Definition

// Define relations with the type-safe macro - NO STRING LITERALS!
relations! {
    User {
        has_many posts: Post via user_id,
    }
    
    Post {
        belongs_to user: User via user_id,
    }
}

Schema Migration

// Create tables and let d1-rs handle relation setup
let migration = SchemaMigration::new("create_blog_tables".to_string())
    .create_table("users")
        .integer("id").primary_key().auto_increment().build()
        .text("name").not_null().build()
        .text("email").not_null().unique().build()
        .boolean("is_active").default_value(DefaultValue::Boolean(true)).build()
    .build()
    
    .create_table("posts")
        .integer("id").primary_key().auto_increment().build()
        .integer("user_id").not_null().build() // Foreign key
        .text("title").not_null().build()
        .text("content").not_null().build()
        .boolean("is_published").default_value(DefaultValue::Boolean(false)).build()
        .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
    .build();

migration.execute(&db).await?;

Using Type-Safe Association Methods

From Parent to Children (One-to-Many)

// Get user's posts - completely type-safe!
let user = User::find(&db, 1).await?.unwrap();
let posts = user.posts().all(&db).await?;

println!("User {} has {} posts", user.name, posts.len());
for post in posts {
    println!("  - {}: {}", post.title, 
        if post.is_published { "Published" } else { "Draft" });
}

// Count posts without loading them
let post_count = user.posts().count(&db).await?;
println!("User has {} total posts", post_count);

// Get first/latest post
let latest_post = user.posts().first(&db).await?;
if let Some(post) = latest_post {
    println!("Latest post: {}", post.title);
}

From Child to Parent (Belongs-to)

// Get post's author - type-safe!
let post = Post::find(&db, 1).await?.unwrap();
let author = post.user().first(&db).await?;

if let Some(user) = author {
    println!("Post '{}' was written by {}", post.title, user.name);
}

Working with Loaded Data

Check if related data is already loaded to avoid unnecessary queries:

// Check if posts are already loaded
if let Some(loaded_posts) = user.posts().loaded() {
    println!("Already have {} posts loaded", loaded_posts.len());
    // Use loaded data without database query
    for post in loaded_posts.iter().take(3) {
        println!("  - {}", post.title);
    }
} else {
    // This will query the database
    let posts = user.posts().all(&db).await?;
    println!("{} has {} posts", user.name, posts.len());
}

Note: Full eager loading functionality is planned for future releases.

Creating Posts for a User

// Create user first
let user = User::create()
    .set_name("Alice Johnson".to_string())
    .set_email("alice@example.com".to_string())
    .set_is_active(true)
    .save(&db)
    .await?;

// Create multiple posts for the user
let post_data = vec![
    ("Getting Started with Rust", "Rust is an amazing systems programming language..."),
    ("Advanced Rust Patterns", "Let's explore some advanced concepts..."),
    ("Web Development with Rust", "Building web applications in Rust..."),
];

for (title, content) in post_data {
    Post::create()
        .set_user_id(user.id) // Set the foreign key
        .set_title(title.to_string())
        .set_content(content.to_string())
        .set_is_published(false)
        .set_created_at(Utc::now())
        .save(&db)
        .await?;
}

// Verify the relationship works
let user_posts = user.posts().all(&db).await?;
println!("Created user with {} posts", user_posts.len());

Advanced Patterns with Association Methods

Custom Helper Methods

Combine the type-safe association methods with custom logic:

impl User {
    // Get only published posts using association method + filtering
    pub async fn published_posts(&self, db: &D1Client) -> Result<Vec<Post>> {
        Post::query()
            .where_user_id_eq(self.id)
            .where_is_published_eq(true)
            .order_by_created_at_desc()
            .all(db)
            .await
    }
    
    // Get recent posts
    pub async fn recent_posts(&self, db: &D1Client, days: i64) -> Result<Vec<Post>> {
        let cutoff = Utc::now() - chrono::Duration::days(days);
        
        Post::query()
            .where_user_id_eq(self.id)
            .where_created_at_gte(cutoff)
            .order_by_created_at_desc()
            .all(db)
            .await
    }
    
    // Use association method for simple counting
    pub async fn post_count(&self, db: &D1Client) -> Result<i64> {
        self.posts().count(db).await
    }
    
    // Combine association with query filtering
    pub async fn published_post_count(&self, db: &D1Client) -> Result<i64> {
        Post::query()
            .where_user_id_eq(self.id)
            .where_is_published_eq(true)
            .count(db)
            .await
    }
    
    // Check if user has any posts (using association method)
    pub async fn has_posts(&self, db: &D1Client) -> Result<bool> {
        let count = self.posts().count(db).await?;
        Ok(count > 0)
    }
}
impl User {
    // Publish all user's draft posts
    pub async fn publish_all_drafts(&self, db: &D1Client) -> Result<Vec<Post>> {
        // Use association method to get related posts, then filter
        let all_posts = self.posts().all(db).await?;
        let drafts: Vec<_> = all_posts.into_iter()
            .filter(|p| !p.is_published)
            .collect();
        
        let mut published = Vec::new();
        for draft in drafts {
            let updated = Post::update(draft.id)
                .set_is_published(true)
                .save(db)
                .await?;
            published.push(updated);
        }
        
        Ok(published)
    }
    
    // Delete all user's posts using association method
    pub async fn delete_all_posts(&self, db: &D1Client) -> Result<i64> {
        let posts = self.posts().all(db).await?;
        let count = posts.len() as i64;
        
        for post in posts {
            Post::delete(db, post.id).await?;
        }
        
        Ok(count)
    }
    
    // Get post statistics using association methods
    pub async fn post_stats(&self, db: &D1Client) -> Result<(i64, i64, i64)> {
        let all_posts = self.posts().all(db).await?;
        let total = all_posts.len() as i64;
        let published = all_posts.iter().filter(|p| p.is_published).count() as i64;
        let drafts = total - published;
        
        Ok((total, published, drafts))
    }
}

Self-Referencing Relations (Hierarchical Data)

Categories with Subcategories

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
#[table(name = "categories")]
pub struct Category {
    #[primary_key]
    pub id: i64,
    pub parent_id: Option<i64>, // Self-referencing foreign key
    pub name: String,
    pub description: Option<String>,
}

// Define self-referencing relations
relations! {
    Category {
        has_many subcategories: Category via parent_id,
        belongs_to parent: Category via parent_id,
    }
}

// Schema migration
let migration = SchemaMigration::new("create_categories".to_string())
    .create_table("categories")
        .integer("id").primary_key().auto_increment().build()
        .integer("parent_id").build() // Optional parent
        .text("name").not_null().build()
        .text("description").build()
    .build();

impl Category {
    // Get subcategories using type-safe association method
    pub async fn get_subcategories(&self, db: &D1Client) -> Result<Vec<Category>> {
        self.subcategories().all(db).await
    }
    
    // Get parent category using type-safe association method  
    pub async fn get_parent(&self, db: &D1Client) -> Result<Option<Category>> {
        self.parent().first(db).await
    }
    
    // Count direct children
    pub async fn child_count(&self, db: &D1Client) -> Result<i64> {
        self.subcategories().count(db).await
    }
    
    // Get root categories (no parent)
    pub async fn roots(db: &D1Client) -> Result<Vec<Category>> {
        Category::query()
            .where_parent_id_is_null()
            .order_by_name_asc()
            .all(db)
            .await
    }
    
    // Check if this category has children
    pub async fn has_children(&self, db: &D1Client) -> Result<bool> {
        let count = self.subcategories().count(db).await?;
        Ok(count > 0)
    }
}

Performance Optimization

Efficient Querying with Association Methods

// Prefer counting over loading all records
let post_count = user.posts().count(&db).await?; // Efficient
let posts = user.posts().all(&db).await?; // Less efficient for counting
let count = posts.len(); // Avoid this pattern

// Use first() when you only need one record
let latest_post = user.posts().first(&db).await?; // Efficient

// Check loaded data first
if let Some(loaded_posts) = user.posts().loaded() {
    // Use loaded data - no database query
    println!("User has {} posts", loaded_posts.len());
} else {
    // Query database only when needed
    let count = user.posts().count(&db).await?;
    println!("User has {} posts", count);
}

Pagination with Association Context

impl User {
    // Paginated posts while maintaining association context
    pub async fn posts_paginated(
        &self, 
        db: &D1Client, 
        page: i64, 
        per_page: i64
    ) -> Result<Vec<Post>> {
        Post::query()
            .where_user_id_eq(self.id)
            .order_by_created_at_desc()
            .limit(per_page)
            .offset(page * per_page)
            .all(db)
            .await
    }
    
    // Get total pages for pagination UI
    pub async fn post_page_count(&self, db: &D1Client, per_page: i64) -> Result<i64> {
        let total = self.posts().count(db).await?;
        Ok((total + per_page - 1) / per_page) // Ceiling division
    }
}

// Usage
let user = User::find(&db, 1).await?.unwrap();
let page_1 = user.posts_paginated(&db, 0, 10).await?; // First 10 posts
let total_pages = user.post_page_count(&db, 10).await?;
println!("Page 1 of {} pages", total_pages);

Working with Association Data

// Transform association data efficiently
#[derive(Debug, Serialize, Deserialize)]
pub struct PostSummary {
    pub id: i64,
    pub title: String,
    pub is_published: bool,
    pub created_at: DateTime<Utc>,
}

impl User {
    // Use association method then transform results
    pub async fn post_summaries(&self, db: &D1Client, limit: Option<i64>) -> Result<Vec<PostSummary>> {
        // Get posts using type-safe association method
        let posts = if let Some(limit_val) = limit {
            Post::query()
                .where_user_id_eq(self.id)
                .order_by_created_at_desc()
                .limit(limit_val)
                .all(db)
                .await?
        } else {
            self.posts().all(db).await?
        };
        
        let summaries = posts.into_iter().map(|p| PostSummary {
            id: p.id,
            title: p.title,
            is_published: p.is_published,
            created_at: p.created_at,
        }).collect();
        
        Ok(summaries)
    }
    
    // Quick check for user activity
    pub async fn is_active_blogger(&self, db: &D1Client) -> Result<bool> {
        let recent_posts = Post::query()
            .where_user_id_eq(self.id)
            .where_created_at_gte(Utc::now() - chrono::Duration::days(30))
            .count(db)
            .await?;
        
        Ok(recent_posts >= 3) // Active if 3+ posts in last 30 days
    }
}

Common Patterns

Multi-Level Blog System

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct Blog {
    #[primary_key]
    pub id: i64,
    pub user_id: i64,
    pub name: String,
    pub description: String,
    pub is_public: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct BlogPost {
    #[primary_key]
    pub id: i64,
    pub blog_id: i64,
    pub title: String,
    pub content: String,
    pub status: String, // "draft", "published", "archived"
    pub published_at: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
}

// Define the complete relationship chain
relations! {
    User {
        has_many blogs: Blog via user_id,
    }
    
    Blog {
        belongs_to user: User via user_id,
        has_many posts: BlogPost via blog_id,
    }
    
    BlogPost {
        belongs_to blog: Blog via blog_id,
    }
}

impl Blog {
    // Use association method + filtering
    pub async fn published_posts(&self, db: &D1Client) -> Result<Vec<BlogPost>> {
        BlogPost::query()
            .where_blog_id_eq(self.id)
            .where_status_eq("published".to_string())
            .where_published_at_is_not_null()
            .order_by_published_at_desc()
            .all(db)
            .await
    }
    
    // Count posts using association method
    pub async fn total_posts(&self, db: &D1Client) -> Result<i64> {
        self.posts().count(db).await
    }
}

impl User {
    // Get all posts across all user's blogs
    pub async fn all_blog_posts(&self, db: &D1Client) -> Result<Vec<BlogPost>> {
        let user_blogs = self.blogs().all(db).await?;
        let mut all_posts = Vec::new();
        
        for blog in user_blogs {
            let mut blog_posts = blog.posts().all(db).await?;
            all_posts.append(&mut blog_posts);
        }
        
        Ok(all_posts)
    }
}

E-commerce Order System

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct Customer {
    #[primary_key]
    pub id: i64,
    pub email: String,
    pub name: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct Order {
    #[primary_key]
    pub id: i64,
    pub customer_id: i64,
    pub status: String,
    pub total: f64,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct OrderItem {
    #[primary_key]
    pub id: i64,
    pub order_id: i64,
    pub product_name: String,
    pub quantity: i32,
    pub unit_price: f64,
}

// Define the complete relationship chain
relations! {
    Customer {
        has_many orders: Order via customer_id,
    }
    
    Order {
        belongs_to customer: Customer via customer_id,
        has_many items: OrderItem via order_id,
    }
    
    OrderItem {
        belongs_to order: Order via order_id,
    }
}

impl Customer {
    // Use association method for order history
    pub async fn order_history(&self, db: &D1Client) -> Result<Vec<Order>> {
        self.orders().all(db).await
    }
    
    pub async fn total_spent(&self, db: &D1Client) -> Result<f64> {
        let orders = self.orders().all(db).await?;
        Ok(orders.iter().map(|o| o.total).sum())
    }
    
    // Count orders using association method
    pub async fn order_count(&self, db: &D1Client) -> Result<i64> {
        self.orders().count(db).await
    }
    
    // Check if customer has any orders
    pub async fn has_orders(&self, db: &D1Client) -> Result<bool> {
        let count = self.orders().count(db).await?;
        Ok(count > 0)
    }
}

impl Order {
    // Use association method for order items
    pub async fn get_items(&self, db: &D1Client) -> Result<Vec<OrderItem>> {
        self.items().all(db).await
    }
    
    // Count items using association method
    pub async fn item_count(&self, db: &D1Client) -> Result<i64> {
        self.items().count(db).await
    }
    
    // Get order owner using association method
    pub async fn get_customer(&self, db: &D1Client) -> Result<Option<Customer>> {
        self.customer().first(db).await
    }
}

Next Steps

The type-safe association methods eliminate the need for string literals and provide compile-time safety that makes relationships much more reliable and maintainable.

Many-to-Many Relations

Many-to-many relations represent complex relationships where multiple records in one table can be associated with multiple records in another table. These are implemented using junction tables.

Basic Many-to-Many

Schema Definition

// Posts have many Categories (and Categories have many Posts)
let migration = SchemaMigration::new("create_post_categories".to_string())
    .create_table("posts")
        .integer("id").primary_key().auto_increment().build()
        .text("title").not_null().build()
        .text("content").not_null().build()
        .boolean("is_published").default_value(DefaultValue::Boolean(false)).build()
    .build()
    
    .create_table("categories")
        .integer("id").primary_key().auto_increment().build()
        .text("name").not_null().unique().build()
        .text("description").build()
    .build()
    
    // Junction table for many-to-many relationship
    .create_table("post_categories")
        .integer("id").primary_key().auto_increment().build()
        .integer("post_id").not_null().build()
        .integer("category_id").not_null().build()
        .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
    .build()
    
    // Define the many-to-many relation
    .create_relation("post_categories_rel", "posts", "categories")
        .many_to_many("post_categories", "post_id", "id", "category_id", "id")
    .build();

migration.execute(&db).await?;

Entity Definitions

use d1_rs::*;
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};

#[derive(Debug, Serialize, Deserialize, Clone, Entity, RelationalEntity, PartialEq)]
pub struct Post {
    #[primary_key]
    pub id: i64,
    pub title: String,
    pub content: String,
    pub is_published: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone, Entity, RelationalEntity, PartialEq)]
#[table(name = "categories")]
pub struct Category {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub description: Option<String>,
}

// Junction table entity (optional - useful for additional data)
#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct PostCategory {
    #[primary_key]
    pub id: i64,
    pub post_id: i64,
    pub category_id: i64,
    pub created_at: DateTime<Utc>,
}

Traversing Many-to-Many Relations

From Post to Categories

// Get all categories for a post
let post = Post::find(&db, 1).await?.unwrap();
let categories = post.traverse::<Category>(&db, "categories").await?;

println!("Post '{}' is in {} categories:", post.title, categories.len());
for category in categories {
    println!("  - {}", category.name);
}

From Category to Posts

// Get all posts in a category
let category = Category::find(&db, 1).await?.unwrap();
let posts = category.traverse::<Post>(&db, "posts").await?;

println!("Category '{}' has {} posts:", category.name, posts.len());
for post in posts {
    println!("  - {}", post.title);
}

Eager Loading

Load posts with their categories efficiently:

// Load posts with all their categories
let posts_with_categories = Post::query()
    .with(vec!["categories"])
    .all(&db)
    .await?;

for post in posts_with_categories {
    let categories = post.traverse::<Category>(&db, "categories").await?;
    
    let category_names: Vec<String> = categories.iter()
        .map(|c| c.name.clone())
        .collect();
    
    println!("{}: [{}]", post.title, category_names.join(", "));
}

Managing Associations

Adding Categories to Posts

impl Post {
    // Add a category to this post
    pub async fn add_category(&self, db: &D1Client, category_id: i64) -> Result<PostCategory> {
        // Check if association already exists
        let existing = PostCategory::query()
            .where_post_id_eq(self.id)
            .where_category_id_eq(category_id)
            .first(db)
            .await?;
        
        if existing.is_some() {
            return Err(D1RsError::Database("Category already associated with post".to_string()));
        }
        
        PostCategory::create()
            .set_post_id(self.id)
            .set_category_id(category_id)
            .set_created_at(Utc::now())
            .save(db)
            .await
    }
    
    // Remove a category from this post
    pub async fn remove_category(&self, db: &D1Client, category_id: i64) -> Result<bool> {
        let association = PostCategory::query()
            .where_post_id_eq(self.id)
            .where_category_id_eq(category_id)
            .first(db)
            .await?;
        
        if let Some(assoc) = association {
            PostCategory::delete(db, assoc.id).await?;
            Ok(true)
        } else {
            Ok(false)
        }
    }
    
    // Set all categories for this post (replaces existing)
    pub async fn set_categories(&self, db: &D1Client, category_ids: Vec<i64>) -> Result<()> {
        // Remove all existing associations
        let existing = PostCategory::query()
            .where_post_id_eq(self.id)
            .all(db)
            .await?;
        
        for assoc in existing {
            PostCategory::delete(db, assoc.id).await?;
        }
        
        // Add new associations
        for category_id in category_ids {
            self.add_category(db, category_id).await?;
        }
        
        Ok(())
    }
    
    // Get category IDs (useful for forms/APIs)
    pub async fn category_ids(&self, db: &D1Client) -> Result<Vec<i64>> {
        let associations = PostCategory::query()
            .where_post_id_eq(self.id)
            .all(db)
            .await?;
        
        Ok(associations.into_iter().map(|a| a.category_id).collect())
    }
}

Category Management

impl Category {
    // Add a post to this category
    pub async fn add_post(&self, db: &D1Client, post_id: i64) -> Result<PostCategory> {
        let post = Post::find(db, post_id).await?
            .ok_or(D1RsError::NotFound)?;
        
        post.add_category(db, self.id).await
    }
    
    // Get posts in this category with filtering
    pub async fn published_posts(&self, db: &D1Client) -> Result<Vec<Post>> {
        let associations = PostCategory::query()
            .where_category_id_eq(self.id)
            .all(db)
            .await?;
        
        let mut posts = Vec::new();
        for assoc in associations {
            if let Some(post) = Post::find(db, assoc.post_id).await? {
                if post.is_published {
                    posts.push(post);
                }
            }
        }
        
        Ok(posts)
    }
    
    // Get post count in category
    pub async fn post_count(&self, db: &D1Client) -> Result<i64> {
        PostCategory::query()
            .where_category_id_eq(self.id)
            .count(db)
            .await
    }
    
    // Get categories ordered by post count
    pub async fn popular_categories(db: &D1Client, limit: i64) -> Result<Vec<(Category, i64)>> {
        let categories = Category::query().all(db).await?;
        let mut category_counts = Vec::new();
        
        for category in categories {
            let count = category.post_count(db).await?;
            category_counts.push((category, count));
        }
        
        // Sort by count descending
        category_counts.sort_by(|a, b| b.1.cmp(&a.1));
        category_counts.truncate(limit as usize);
        
        Ok(category_counts)
    }
}

Advanced Patterns

Tagged System

#[derive(Entity, RelationalEntity)]
pub struct Article {
    #[primary_key]
    pub id: i64,
    pub title: String,
    pub content: String,
    pub author: String,
    pub published_at: DateTime<Utc>,
}

#[derive(Entity, RelationalEntity)]
pub struct Tag {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub color: String, // Hex color for UI
    pub usage_count: i64, // Denormalized count for performance
}

#[derive(Entity)]
pub struct ArticleTag {
    #[primary_key]
    pub id: i64,
    pub article_id: i64,
    pub tag_id: i64,
    pub added_by: String, // Who tagged it
    pub created_at: DateTime<Utc>,
}

impl Article {
    // Get related articles based on shared tags
    pub async fn related_articles(&self, db: &D1Client, limit: i64) -> Result<Vec<Article>> {
        let my_tags = self.traverse::<Tag>(db, "tags").await?;
        let tag_ids: Vec<i64> = my_tags.iter().map(|t| t.id).collect();
        
        if tag_ids.is_empty() {
            return Ok(Vec::new());
        }
        
        // Find articles that share tags (excluding self)
        let mut related_scores: std::collections::HashMap<i64, usize> = std::collections::HashMap::new();
        
        for tag_id in tag_ids {
            let tag_articles = ArticleTag::query()
                .where_tag_id_eq(tag_id)
                .all(db)
                .await?;
            
            for article_tag in tag_articles {
                if article_tag.article_id != self.id {
                    *related_scores.entry(article_tag.article_id).or_insert(0) += 1;
                }
            }
        }
        
        // Sort by shared tag count and load articles
        let mut scored_articles: Vec<_> = related_scores.into_iter().collect();
        scored_articles.sort_by(|a, b| b.1.cmp(&a.1));
        
        let mut related = Vec::new();
        for (article_id, _score) in scored_articles.into_iter().take(limit as usize) {
            if let Some(article) = Article::find(db, article_id).await? {
                related.push(article);
            }
        }
        
        Ok(related)
    }
}

impl Tag {
    // Update usage count when tags are added/removed
    pub async fn increment_usage(&self, db: &D1Client) -> Result<Tag> {
        Tag::update(self.id)
            .set_usage_count(self.usage_count + 1)
            .save(db)
            .await
    }
    
    pub async fn decrement_usage(&self, db: &D1Client) -> Result<Tag> {
        let new_count = std::cmp::max(0, self.usage_count - 1);
        Tag::update(self.id)
            .set_usage_count(new_count)
            .save(db)
            .await
    }
    
    // Get most popular tags
    pub async fn most_popular(db: &D1Client, limit: i64) -> Result<Vec<Tag>> {
        Tag::query()
            .order_by_usage_count_desc()
            .limit(limit)
            .all(db)
            .await
    }
}

User Permissions System

#[derive(Entity, RelationalEntity)]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub username: String,
    pub email: String,
    pub is_active: bool,
}

#[derive(Entity, RelationalEntity)]
pub struct Role {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub description: String,
}

#[derive(Entity, RelationalEntity)]
pub struct Permission {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub resource: String,
    pub action: String, // "read", "write", "delete", etc.
}

#[derive(Entity)]
pub struct UserRole {
    #[primary_key]
    pub id: i64,
    pub user_id: i64,
    pub role_id: i64,
    pub granted_at: DateTime<Utc>,
    pub granted_by: i64,
}

#[derive(Entity)]
pub struct RolePermission {
    #[primary_key]
    pub id: i64,
    pub role_id: i64,
    pub permission_id: i64,
}

impl User {
    // Get all permissions for a user (through roles)
    pub async fn all_permissions(&self, db: &D1Client) -> Result<Vec<Permission>> {
        let roles = self.traverse::<Role>(db, "roles").await?;
        let mut permissions = Vec::new();
        
        for role in roles {
            let role_permissions = role.traverse::<Permission>(db, "permissions").await?;
            permissions.extend(role_permissions);
        }
        
        // Remove duplicates
        permissions.sort_by_key(|p| p.id);
        permissions.dedup_by_key(|p| p.id);
        
        Ok(permissions)
    }
    
    // Check if user has specific permission
    pub async fn has_permission(&self, db: &D1Client, resource: &str, action: &str) -> Result<bool> {
        let permissions = self.all_permissions(db).await?;
        
        Ok(permissions.iter().any(|p| p.resource == resource && p.action == action))
    }
    
    // Add role to user
    pub async fn add_role(&self, db: &D1Client, role_id: i64, granted_by: i64) -> Result<UserRole> {
        UserRole::create()
            .set_user_id(self.id)
            .set_role_id(role_id)
            .set_granted_at(Utc::now())
            .set_granted_by(granted_by)
            .save(db)
            .await
    }
}

Performance Considerations

Junction Table Optimization

// Optimize junction table with compound indexes
let migration = SchemaMigration::new("optimize_post_categories".to_string())
    .alter_table("post_categories")
        // Compound index for foreign key pair (prevents duplicates efficiently)
        .add_unique_index("idx_post_category_unique", vec!["post_id", "category_id"])
        // Individual indexes for reverse lookups
        .add_index("idx_post_categories_post_id", vec!["post_id"])
        .add_index("idx_post_categories_category_id", vec!["category_id"])
        // Index on created_at for temporal queries
        .add_index("idx_post_categories_created_at", vec!["created_at"])
    .build();

Batch Operations

impl Post {
    // Efficiently set multiple categories at once
    pub async fn set_categories_batch(&self, db: &D1Client, category_ids: Vec<i64>) -> Result<()> {
        // Remove existing associations in batch
        let existing = PostCategory::query()
            .where_post_id_eq(self.id)
            .all(db)
            .await?;
        
        // In a real implementation with transactions:
        // db.transaction(|tx| async {
        //     for assoc in existing {
        //         PostCategory::delete(&tx, assoc.id).await?;
        //     }
        //     
        //     for category_id in category_ids {
        //         PostCategory::create()
        //             .set_post_id(self.id)
        //             .set_category_id(category_id)
        //             .set_created_at(Utc::now())
        //             .save(&tx)
        //             .await?;
        //     }
        //     Ok(())
        // }).await?;
        
        // Current implementation (without transactions)
        for assoc in existing {
            PostCategory::delete(db, assoc.id).await?;
        }
        
        for category_id in category_ids {
            PostCategory::create()
                .set_post_id(self.id)
                .set_category_id(category_id)
                .set_created_at(Utc::now())
                .save(db)
                .await?;
        }
        
        Ok(())
    }
}

Denormalization for Performance

// Add counts to improve query performance
#[derive(Entity, RelationalEntity)]
pub struct OptimizedCategory {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub description: Option<String>,
    pub post_count: i64,        // Denormalized count
    pub published_post_count: i64, // Count of published posts only
    pub last_updated: DateTime<Utc>,
}

impl OptimizedCategory {
    // Recalculate and update counts
    pub async fn refresh_counts(&self, db: &D1Client) -> Result<OptimizedCategory> {
        let total_count = PostCategory::query()
            .where_category_id_eq(self.id)
            .count(db)
            .await?;
        
        // Count published posts
        let associations = PostCategory::query()
            .where_category_id_eq(self.id)
            .all(db)
            .await?;
        
        let mut published_count = 0;
        for assoc in associations {
            if let Some(post) = Post::find(db, assoc.post_id).await? {
                if post.is_published {
                    published_count += 1;
                }
            }
        }
        
        OptimizedCategory::update(self.id)
            .set_post_count(total_count)
            .set_published_post_count(published_count)
            .set_last_updated(Utc::now())
            .save(db)
            .await
    }
}

Common Use Cases

Content Management System

// CMS with pages, authors, and tags
#[derive(Entity, RelationalEntity)]
pub struct Page {
    #[primary_key] pub id: i64,
    pub title: String,
    pub slug: String,
    pub content: String,
    pub status: String, // "draft", "published", "archived"
}

#[derive(Entity, RelationalEntity)]
pub struct Author {
    #[primary_key] pub id: i64,
    pub name: String,
    pub email: String,
}

#[derive(Entity, RelationalEntity)]
pub struct ContentTag {
    #[primary_key] pub id: i64,
    pub name: String,
    pub category: String, // "topic", "format", "difficulty", etc.
}

// Junction tables for many-to-many relationships
#[derive(Entity)] pub struct PageAuthor { /* ... */ }
#[derive(Entity)] pub struct PageTag { /* ... */ }

E-learning Platform

#[derive(Entity, RelationalEntity)]
pub struct Student {
    #[primary_key] pub id: i64,
    pub name: String,
    pub email: String,
}

#[derive(Entity, RelationalEntity)]
pub struct Course {
    #[primary_key] pub id: i64,
    pub title: String,
    pub description: String,
}

#[derive(Entity)]
pub struct Enrollment {
    #[primary_key] pub id: i64,
    pub student_id: i64,
    pub course_id: i64,
    pub enrolled_at: DateTime<Utc>,
    pub completed_at: Option<DateTime<Utc>>,
    pub grade: Option<f64>,
}

impl Student {
    pub async fn enroll_in_course(&self, db: &D1Client, course_id: i64) -> Result<Enrollment> {
        Enrollment::create()
            .set_student_id(self.id)
            .set_course_id(course_id)
            .set_enrolled_at(Utc::now())
            .save(db)
            .await
    }
    
    pub async fn completed_courses(&self, db: &D1Client) -> Result<Vec<Course>> {
        let completed_enrollments = Enrollment::query()
            .where_student_id_eq(self.id)
            .where_completed_at_is_not_null()
            .all(db)
            .await?;
        
        let mut courses = Vec::new();
        for enrollment in completed_enrollments {
            if let Some(course) = Course::find(db, enrollment.course_id).await? {
                courses.push(course);
            }
        }
        
        Ok(courses)
    }
}

Next Steps

Graph Traversal

Boolean Handling

This guide explains how d1-rs handles boolean values, the automatic conversion between Rust's bool type and SQLite's integer storage, and best practices for working with boolean fields.

SQLite Boolean Storage

SQLite doesn't have a native boolean type, so boolean values are stored as integers:

  • true1
  • false0

d1-rs handles this conversion automatically, providing a seamless experience where you work with Rust's bool type while the database stores integers.

Automatic Conversion

Database Schema

When you create a boolean field in your migration:

let migration = SchemaMigration::new("create_users".to_string())
    .create_table("users")
        .integer("id").primary_key().auto_increment().build()
        .text("name").not_null().build()
        .boolean("is_active").default_value(DefaultValue::Boolean(true)).build()
        .boolean("is_verified").default_value(DefaultValue::Boolean(false)).build()
        .boolean("receive_notifications").build() // No default (NULL allowed)
    .build();

This generates SQL like:

CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    is_active INTEGER DEFAULT 1,
    is_verified INTEGER DEFAULT 0,
    receive_notifications INTEGER
);

Entity Definition

In your Rust entity, use standard bool types:

#[derive(Debug, Serialize, Deserialize, Clone, Entity, PartialEq)]
pub struct User {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub is_active: bool,        // Will be stored as INTEGER in database
    pub is_verified: bool,
    pub receive_notifications: Option<bool>, // Optional boolean
}

Conversion Process

d1-rs performs automatic conversion in both directions:

// When saving to database
let user = User::create()
    .set_name("Alice".to_string())
    .set_is_active(true)      // Becomes 1 in database
    .set_is_verified(false)   // Becomes 0 in database
    .save(&db)
    .await?;

// When loading from database
let loaded_user = User::find(&db, user.id).await?.unwrap();
assert_eq!(loaded_user.is_active, true);    // 1 becomes true
assert_eq!(loaded_user.is_verified, false); // 0 becomes false

Query Operations

Boolean Filtering

Use natural boolean values in queries:

// Query for active users
let active_users = User::query()
    .where_is_active_eq(true)    // Automatically converts to WHERE is_active = 1
    .all(&db)
    .await?;

// Query for unverified users
let unverified_users = User::query()
    .where_is_verified_eq(false) // Automatically converts to WHERE is_verified = 0
    .all(&db)
    .await?;

// Query for users who haven't set notification preference
let undecided_users = User::query()
    .where_receive_notifications_is_null()
    .all(&db)
    .await?;

Boolean Updates

Update boolean fields naturally:

// Toggle user verification
let verified_user = User::update(user.id)
    .set_is_verified(true)       // Will store as 1
    .save(&db)
    .await?;

// Disable notifications
let updated_user = User::update(user.id)
    .set_receive_notifications(Some(false)) // Optional boolean
    .save(&db)
    .await?;

Advanced Boolean Patterns

Boolean Flags

Use multiple boolean fields for feature flags:

#[derive(Entity)]
pub struct UserSettings {
    #[primary_key]
    pub id: i64,
    pub user_id: i64,
    
    // Feature flags
    pub dark_mode: bool,
    pub email_notifications: bool,
    pub push_notifications: bool,
    pub marketing_emails: bool,
    pub beta_features: bool,
    
    // Privacy settings
    pub profile_public: bool,
    pub show_activity: bool,
    pub allow_friend_requests: bool,
}

impl UserSettings {
    // Helper methods for common operations
    pub async fn enable_all_notifications(&self, db: &D1Client) -> Result<UserSettings> {
        UserSettings::update(self.id)
            .set_email_notifications(true)
            .set_push_notifications(true)
            .save(db)
            .await
    }
    
    pub async fn privacy_mode(&self, db: &D1Client) -> Result<UserSettings> {
        UserSettings::update(self.id)
            .set_profile_public(false)
            .set_show_activity(false)
            .set_allow_friend_requests(false)
            .save(db)
            .await
    }
    
    // Query methods
    pub async fn users_with_notifications_enabled(db: &D1Client) -> Result<Vec<UserSettings>> {
        UserSettings::query()
            .where_email_notifications_eq(true)
            .where_push_notifications_eq(true)
            .all(db)
            .await
    }
}

State Management

Use booleans for simple state tracking:

#[derive(Entity)]
pub struct Post {
    #[primary_key]
    pub id: i64,
    pub title: String,
    pub content: String,
    
    // State flags
    pub is_published: bool,
    pub is_featured: bool,
    pub is_archived: bool,
    pub comments_enabled: bool,
    pub is_deleted: bool, // Soft delete flag
}

impl Post {
    // State transition methods
    pub async fn publish(&self, db: &D1Client) -> Result<Post> {
        Post::update(self.id)
            .set_is_published(true)
            .save(db)
            .await
    }
    
    pub async fn archive(&self, db: &D1Client) -> Result<Post> {
        Post::update(self.id)
            .set_is_archived(true)
            .set_is_featured(false) // Remove from featured when archived
            .save(db)
            .await
    }
    
    pub async fn soft_delete(&self, db: &D1Client) -> Result<Post> {
        Post::update(self.id)
            .set_is_deleted(true)
            .set_is_published(false)
            .set_is_featured(false)
            .save(db)
            .await
    }
    
    // Query active posts (not deleted or archived)
    pub async fn active_posts(db: &D1Client) -> Result<Vec<Post>> {
        Post::query()
            .where_is_deleted_eq(false)
            .where_is_archived_eq(false)
            .all(db)
            .await
    }
    
    // Featured published posts
    pub async fn featured_posts(db: &D1Client) -> Result<Vec<Post>> {
        Post::query()
            .where_is_published_eq(true)
            .where_is_featured_eq(true)
            .where_is_deleted_eq(false)
            .all(db)
            .await
    }
}

Working with Optional Booleans

Nullable Boolean Fields

Sometimes you need three states: true, false, or unknown/unset:

#[derive(Entity)]
pub struct UserProfile {
    #[primary_key]
    pub id: i64,
    pub user_id: i64,
    
    // Three-state booleans (true/false/null)
    pub wants_newsletter: Option<bool>,    // NULL = hasn't decided
    pub is_looking_for_job: Option<bool>,  // NULL = not specified
    pub available_for_hire: Option<bool>,  // NULL = not set
}

impl UserProfile {
    // Handle three-state logic
    pub async fn users_open_to_newsletter(db: &D1Client) -> Result<Vec<UserProfile>> {
        // Only users who explicitly said yes
        UserProfile::query()
            .where_wants_newsletter_eq(true)
            .all(db)
            .await
    }
    
    pub async fn users_newsletter_undecided(db: &D1Client) -> Result<Vec<UserProfile>> {
        // Users who haven't made a choice
        UserProfile::query()
            .where_wants_newsletter_is_null()
            .all(db)
            .await
    }
    
    pub fn newsletter_preference_string(&self) -> &'static str {
        match self.wants_newsletter {
            Some(true) => "Yes",
            Some(false) => "No", 
            None => "Not specified",
        }
    }
}

Default Values for Optional Booleans

Set sensible defaults in migrations:

let migration = SchemaMigration::new("user_preferences".to_string())
    .create_table("user_preferences")
        .integer("id").primary_key().auto_increment().build()
        .integer("user_id").not_null().build()
        
        // Required booleans with defaults
        .boolean("email_verified").default_value(DefaultValue::Boolean(false)).build()
        .boolean("profile_complete").default_value(DefaultValue::Boolean(false)).build()
        
        // Optional booleans (can be NULL)
        .boolean("marketing_consent").build()  // No default = NULL allowed
        .boolean("data_sharing_consent").build()
        
        // Boolean with explicit NULL default
        .boolean("newsletter_preference").default_value(DefaultValue::Null).build()
    .build();

Boolean Conversion Edge Cases

Handling Integer Values

d1-rs treats any non-zero integer as true:

// These are the standard conversions:
// 0 -> false
// 1 -> true
// Any other integer -> true (though this shouldn't happen in normal usage)

#[cfg(test)]
mod boolean_conversion_tests {
    use super::*;
    
    #[tokio::test]
    async fn test_boolean_storage_and_retrieval() {
        let db = D1Client::new_in_memory().await.unwrap();
        setup_test_schema(&db).await.unwrap();
        
        // Test true value
        let user_true = User::create()
            .set_name("True User".to_string())
            .set_is_active(true)
            .save(&db)
            .await
            .unwrap();
        
        let loaded_true = User::find(&db, user_true.id).await.unwrap().unwrap();
        assert_eq!(loaded_true.is_active, true);
        
        // Test false value
        let user_false = User::create()
            .set_name("False User".to_string())
            .set_is_active(false)
            .save(&db)
            .await
            .unwrap();
        
        let loaded_false = User::find(&db, user_false.id).await.unwrap().unwrap();
        assert_eq!(loaded_false.is_active, false);
    }
    
    #[tokio::test]
    async fn test_optional_boolean_handling() {
        let db = D1Client::new_in_memory().await.unwrap();
        // Setup schema with optional boolean...
        
        // Test None/NULL value
        let profile_none = UserProfile::create()
            .set_user_id(1)
            .save(&db)  // wants_newsletter remains None/NULL
            .await
            .unwrap();
        
        assert_eq!(profile_none.wants_newsletter, None);
        
        // Test Some(true) value
        let profile_true = UserProfile::create()
            .set_user_id(2)
            .set_wants_newsletter(Some(true))
            .save(&db)
            .await
            .unwrap();
        
        assert_eq!(profile_true.wants_newsletter, Some(true));
        
        // Test Some(false) value
        let profile_false = UserProfile::create()
            .set_user_id(3)
            .set_wants_newsletter(Some(false))
            .save(&db)
            .await
            .unwrap();
        
        assert_eq!(profile_false.wants_newsletter, Some(false));
    }
}

Performance Considerations

Indexing Boolean Fields

Boolean fields can be effectively indexed:

let migration = SchemaMigration::new("boolean_indexes".to_string())
    .alter_table("users")
        // Single boolean indexes
        .add_index("idx_users_active", vec!["is_active"])
        .add_index("idx_users_verified", vec!["is_verified"])
        
        // Composite indexes with booleans
        .add_index("idx_users_active_verified", vec!["is_active", "is_verified"])
        .add_index("idx_users_status_created", vec!["is_active", "created_at"])
    .build();

Efficient Boolean Queries

Structure queries to take advantage of indexes:

impl User {
    // Efficient: Uses index on is_active
    pub async fn active_users_count(db: &D1Client) -> Result<i64> {
        User::query()
            .where_is_active_eq(true)
            .count(db)
            .await
    }
    
    // Efficient: Uses compound index
    pub async fn active_verified_users(db: &D1Client) -> Result<Vec<User>> {
        User::query()
            .where_is_active_eq(true)
            .where_is_verified_eq(true)
            .order_by_created_at_desc()
            .all(db)
            .await
    }
    
    // Less efficient: Multiple separate conditions
    pub async fn complex_user_filter(db: &D1Client) -> Result<Vec<User>> {
        User::query()
            .where_is_active_eq(true)
            .where_is_verified_eq(true)
            .where_receive_notifications_eq(true)
            .all(db)
            .await
    }
}

Migration Patterns

Adding Boolean Fields

When adding boolean fields to existing tables:

let migration = SchemaMigration::new("add_user_preferences".to_string())
    .alter_table("users")
        // Add with sensible default for existing records
        .add_column("email_notifications", ColumnType::Boolean)
            .default_value(DefaultValue::Boolean(true))
            .build()
        
        // Add optional boolean (existing records will have NULL)
        .add_column("marketing_consent", ColumnType::Boolean)
            .build()
    .build();

Boolean Field Evolution

Evolving boolean logic over time:

// Migration 1: Simple active flag
let migration_v1 = SchemaMigration::new("user_status_v1".to_string())
    .create_table("users")
        .boolean("is_active").default_value(DefaultValue::Boolean(true)).build()
    .build();

// Migration 2: More granular status
let migration_v2 = SchemaMigration::new("user_status_v2".to_string())
    .alter_table("users")
        .add_column("is_suspended", ColumnType::Boolean)
            .default_value(DefaultValue::Boolean(false))
            .build()
        .add_column("is_banned", ColumnType::Boolean)
            .default_value(DefaultValue::Boolean(false))
            .build()
    .build();

// Migration 3: Consolidate into enum (future enhancement)
// This would involve string fields instead of multiple booleans

Best Practices

Boolean Field Design

  1. Use descriptive names: is_active instead of active
  2. Default to safe values: Usually false for permissions, true for enabled features
  3. Consider three-state logic: Use Option<bool> when "unknown" is meaningful
  4. Group related booleans: Consider separate settings tables for complex boolean groups

Query Optimization

  1. Index frequently queried boolean fields
  2. Use compound indexes for common boolean combinations
  3. Structure WHERE clauses to use indexes effectively
  4. Consider denormalization for complex boolean logic

Testing

Always test boolean conversion:

#[cfg(test)]
mod tests {
    use super::*;
    
    #[tokio::test]
    async fn test_all_boolean_combinations() {
        let db = D1Client::new_in_memory().await.unwrap();
        
        // Test all combinations of boolean values
        let combinations = [
            (true, true, Some(true)),
            (true, false, Some(false)),
            (false, true, None),
            (false, false, Some(true)),
        ];
        
        for (is_active, is_verified, notifications) in combinations {
            let user = User::create()
                .set_name("Test".to_string())
                .set_is_active(is_active)
                .set_is_verified(is_verified)
                .set_receive_notifications(notifications)
                .save(&db)
                .await
                .unwrap();
            
            // Verify values are preserved
            assert_eq!(user.is_active, is_active);
            assert_eq!(user.is_verified, is_verified);
            assert_eq!(user.receive_notifications, notifications);
        }
    }
}

Next Steps

Type Safety

Testing Strategies

This guide covers comprehensive testing strategies for d1-rs applications, from unit tests to integration tests and performance testing.

Test Environment Setup

In-Memory Database Testing

d1-rs makes testing easy with automatic SQLite fallback:

use d1_rs::*;
use tokio;

#[tokio::test]
async fn test_user_crud() {
    // Creates an in-memory SQLite database
    let db = D1Client::new_in_memory().await.unwrap();
    
    // Run migrations
    setup_test_schema(&db).await.unwrap();
    
    // Your test code here
    let user = User::create()
        .set_name("Test User".to_string())
        .set_email("test@example.com".to_string())
        .set_is_active(true)
        .save(&db)
        .await
        .unwrap();
    
    assert_eq!(user.name, "Test User");
    assert_eq!(user.email, "test@example.com");
    assert!(user.is_active);
}

async fn setup_test_schema(db: &D1Client) -> Result<()> {
    let migration = SchemaMigration::new("test_schema".to_string())
        .create_table("users")
            .integer("id").primary_key().auto_increment().build()
            .text("name").not_null().build()
            .text("email").not_null().unique().build()
            .boolean("is_active").default_value(DefaultValue::Boolean(true)).build()
            .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .build();
    
    migration.execute(db).await
}

Test Utilities

Create reusable test utilities:

// tests/common/mod.rs
use d1_rs::*;
use chrono::{DateTime, Utc};

pub struct TestDatabase {
    pub db: D1Client,
}

impl TestDatabase {
    pub async fn new() -> Self {
        let db = D1Client::new_in_memory().await.unwrap();
        setup_complete_schema(&db).await.unwrap();
        Self { db }
    }
    
    pub async fn with_test_data() -> Self {
        let test_db = Self::new().await;
        create_test_data(&test_db.db).await.unwrap();
        test_db
    }
}

async fn setup_complete_schema(db: &D1Client) -> Result<()> {
    let migration = SchemaMigration::new("complete_test_schema".to_string())
        .create_table("users")
            .integer("id").primary_key().auto_increment().build()
            .text("name").not_null().build()
            .text("email").not_null().unique().build()
            .boolean("is_active").default_value(DefaultValue::Boolean(true)).build()
            .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .build()
        
        .create_table("posts")
            .integer("id").primary_key().auto_increment().build()
            .integer("user_id").not_null().build()
            .text("title").not_null().build()
            .text("content").not_null().build()
            .boolean("is_published").default_value(DefaultValue::Boolean(false)).build()
            .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .build()
        
        .create_table("categories")
            .integer("id").primary_key().auto_increment().build()
            .text("name").not_null().unique().build()
            .text("description").build()
        .build()
        
        .create_table("post_categories")
            .integer("id").primary_key().auto_increment().build()
            .integer("post_id").not_null().build()
            .integer("category_id").not_null().build()
            .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .build()
        
        .create_relation("user_posts", "users", "posts")
            .one_to_many("user_id", "id")
        .build()
        
        .create_relation("post_categories_rel", "posts", "categories")
            .many_to_many("post_categories", "post_id", "id", "category_id", "id")
        .build();
    
    migration.execute(db).await
}

async fn create_test_data(db: &D1Client) -> Result<()> {
    // Create test users
    let alice = User::create()
        .set_name("Alice Johnson".to_string())
        .set_email("alice@example.com".to_string())
        .set_is_active(true)
        .save(db)
        .await?;
    
    let bob = User::create()
        .set_name("Bob Smith".to_string())
        .set_email("bob@example.com".to_string())
        .set_is_active(true)
        .save(db)
        .await?;
    
    // Create test categories
    let tech = Category::create()
        .set_name("Technology".to_string())
        .set_description(Some("Tech-related posts".to_string()))
        .save(db)
        .await?;
    
    let lifestyle = Category::create()
        .set_name("Lifestyle".to_string())
        .set_description(Some("Lifestyle posts".to_string()))
        .save(db)
        .await?;
    
    // Create test posts
    let post1 = Post::create()
        .set_user_id(alice.id)
        .set_title("Getting Started with Rust".to_string())
        .set_content("Rust is an amazing language...".to_string())
        .set_is_published(true)
        .set_created_at(Utc::now())
        .save(db)
        .await?;
    
    let post2 = Post::create()
        .set_user_id(bob.id)
        .set_title("Coffee Brewing Tips".to_string())
        .set_content("Here's how to brew the perfect cup...".to_string())
        .set_is_published(true)
        .set_created_at(Utc::now())
        .save(db)
        .await?;
    
    // Create post-category associations
    PostCategory::create()
        .set_post_id(post1.id)
        .set_category_id(tech.id)
        .set_created_at(Utc::now())
        .save(db)
        .await?;
    
    PostCategory::create()
        .set_post_id(post2.id)
        .set_category_id(lifestyle.id)
        .set_created_at(Utc::now())
        .save(db)
        .await?;
    
    Ok(())
}

// Test data builders for flexible test setup
pub struct UserBuilder {
    name: String,
    email: String,
    is_active: bool,
}

impl UserBuilder {
    pub fn new() -> Self {
        Self {
            name: "Test User".to_string(),
            email: "test@example.com".to_string(),
            is_active: true,
        }
    }
    
    pub fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self
    }
    
    pub fn email(mut self, email: &str) -> Self {
        self.email = email.to_string();
        self
    }
    
    pub fn inactive(mut self) -> Self {
        self.is_active = false;
        self
    }
    
    pub async fn save(self, db: &D1Client) -> Result<User> {
        User::create()
            .set_name(self.name)
            .set_email(self.email)
            .set_is_active(self.is_active)
            .save(db)
            .await
    }
}

pub struct PostBuilder {
    user_id: i64,
    title: String,
    content: String,
    is_published: bool,
}

impl PostBuilder {
    pub fn for_user(user_id: i64) -> Self {
        Self {
            user_id,
            title: "Test Post".to_string(),
            content: "Test content".to_string(),
            is_published: false,
        }
    }
    
    pub fn title(mut self, title: &str) -> Self {
        self.title = title.to_string();
        self
    }
    
    pub fn content(mut self, content: &str) -> Self {
        self.content = content.to_string();
        self
    }
    
    pub fn published(mut self) -> Self {
        self.is_published = true;
        self
    }
    
    pub async fn save(self, db: &D1Client) -> Result<Post> {
        Post::create()
            .set_user_id(self.user_id)
            .set_title(self.title)
            .set_content(self.content)
            .set_is_published(self.is_published)
            .set_created_at(Utc::now())
            .save(db)
            .await
    }
}

Unit Testing

Entity CRUD Tests

Test basic entity operations:

#[cfg(test)]
mod entity_tests {
    use super::*;
    use crate::common::*;
    
    #[tokio::test]
    async fn test_user_creation() {
        let test_db = TestDatabase::new().await;
        
        let user = UserBuilder::new()
            .name("Alice")
            .email("alice@test.com")
            .save(&test_db.db)
            .await
            .unwrap();
        
        assert_eq!(user.name, "Alice");
        assert_eq!(user.email, "alice@test.com");
        assert!(user.is_active);
        assert!(user.id > 0);
    }
    
    #[tokio::test]
    async fn test_user_update() {
        let test_db = TestDatabase::new().await;
        
        let user = UserBuilder::new()
            .name("Original Name")
            .save(&test_db.db)
            .await
            .unwrap();
        
        let updated = User::update(user.id)
            .set_name("Updated Name".to_string())
            .set_is_active(false)
            .save(&test_db.db)
            .await
            .unwrap();
        
        assert_eq!(updated.name, "Updated Name");
        assert!(!updated.is_active);
        assert_eq!(updated.id, user.id);
    }
    
    #[tokio::test]
    async fn test_user_deletion() {
        let test_db = TestDatabase::new().await;
        
        let user = UserBuilder::new().save(&test_db.db).await.unwrap();
        let user_id = user.id;
        
        User::delete(&test_db.db, user_id).await.unwrap();
        
        let found = User::find(&test_db.db, user_id).await.unwrap();
        assert!(found.is_none());
    }
    
    #[tokio::test]
    async fn test_unique_constraint_violation() {
        let test_db = TestDatabase::new().await;
        
        // Create first user
        UserBuilder::new()
            .email("duplicate@test.com")
            .save(&test_db.db)
            .await
            .unwrap();
        
        // Try to create second user with same email
        let result = UserBuilder::new()
            .name("Different Name")
            .email("duplicate@test.com")
            .save(&test_db.db)
            .await;
        
        assert!(result.is_err());
    }
}

Query Builder Tests

Test query building and filtering:

#[cfg(test)]
mod query_tests {
    use super::*;
    use crate::common::*;
    
    #[tokio::test]
    async fn test_where_clauses() {
        let test_db = TestDatabase::with_test_data().await;
        
        // Test exact match
        let alice = User::query()
            .where_name_eq("Alice Johnson".to_string())
            .first(&test_db.db)
            .await
            .unwrap();
        assert!(alice.is_some());
        
        // Test contains
        let users_with_john = User::query()
            .where_name_contains("John")
            .all(&test_db.db)
            .await
            .unwrap();
        assert_eq!(users_with_john.len(), 1);
        
        // Test boolean filtering
        let active_users = User::query()
            .where_is_active_eq(true)
            .all(&test_db.db)
            .await
            .unwrap();
        assert!(active_users.len() >= 2);
    }
    
    #[tokio::test]
    async fn test_ordering() {
        let test_db = TestDatabase::new().await;
        
        // Create users in specific order
        let user_c = UserBuilder::new().name("Charlie").save(&test_db.db).await.unwrap();
        let user_a = UserBuilder::new().name("Alice").email("alice@test.com").save(&test_db.db).await.unwrap();
        let user_b = UserBuilder::new().name("Bob").email("bob@test.com").save(&test_db.db).await.unwrap();
        
        // Test ascending order
        let users_asc = User::query()
            .order_by_name_asc()
            .all(&test_db.db)
            .await
            .unwrap();
        
        assert_eq!(users_asc[0].name, "Alice");
        assert_eq!(users_asc[1].name, "Bob");
        assert_eq!(users_asc[2].name, "Charlie");
        
        // Test descending order
        let users_desc = User::query()
            .order_by_name_desc()
            .all(&test_db.db)
            .await
            .unwrap();
        
        assert_eq!(users_desc[0].name, "Charlie");
        assert_eq!(users_desc[1].name, "Bob");
        assert_eq!(users_desc[2].name, "Alice");
    }
    
    #[tokio::test]
    async fn test_pagination() {
        let test_db = TestDatabase::new().await;
        
        // Create multiple users
        for i in 1..=10 {
            UserBuilder::new()
                .name(&format!("User {}", i))
                .email(&format!("user{}@test.com", i))
                .save(&test_db.db)
                .await
                .unwrap();
        }
        
        // Test first page
        let page1 = User::query()
            .order_by_id_asc()
            .limit(3)
            .offset(0)
            .all(&test_db.db)
            .await
            .unwrap();
        assert_eq!(page1.len(), 3);
        
        // Test second page
        let page2 = User::query()
            .order_by_id_asc()
            .limit(3)
            .offset(3)
            .all(&test_db.db)
            .await
            .unwrap();
        assert_eq!(page2.len(), 3);
        assert_ne!(page1[0].id, page2[0].id);
    }
    
    #[tokio::test]
    async fn test_count_query() {
        let test_db = TestDatabase::new().await;
        
        // Create some users
        for i in 1..=5 {
            UserBuilder::new()
                .name(&format!("User {}", i))
                .email(&format!("user{}@test.com", i))
                .save(&test_db.db)
                .await
                .unwrap();
        }
        
        let total_count = User::query().count(&test_db.db).await.unwrap();
        assert_eq!(total_count, 5);
        
        let active_count = User::query()
            .where_is_active_eq(true)
            .count(&test_db.db)
            .await
            .unwrap();
        assert_eq!(active_count, 5);
    }
}

Relationship Testing

One-to-Many Relationships

#[cfg(test)]
mod relationship_tests {
    use super::*;
    use crate::common::*;
    
    #[tokio::test]
    async fn test_one_to_many_traversal() {
        let test_db = TestDatabase::new().await;
        
        let user = UserBuilder::new().save(&test_db.db).await.unwrap();
        
        // Create posts for the user
        let post1 = PostBuilder::for_user(user.id)
            .title("First Post")
            .published()
            .save(&test_db.db)
            .await
            .unwrap();
        
        let post2 = PostBuilder::for_user(user.id)
            .title("Second Post")
            .save(&test_db.db)
            .await
            .unwrap();
        
        // Test traversal from user to posts
        let posts = user.traverse::<Post>(&test_db.db, "posts").await.unwrap();
        assert_eq!(posts.len(), 2);
        
        let titles: Vec<_> = posts.iter().map(|p| &p.title).collect();
        assert!(titles.contains(&&"First Post".to_string()));
        assert!(titles.contains(&&"Second Post".to_string()));
        
        // Test reverse traversal from post to user
        let users = post1.traverse::<User>(&test_db.db, "user").await.unwrap();
        assert_eq!(users.len(), 1);
        assert_eq!(users[0].id, user.id);
    }
    
    #[tokio::test]
    async fn test_eager_loading() {
        let test_db = TestDatabase::with_test_data().await;
        
        let users_with_posts = User::query()
            .with(vec!["posts"])
            .all(&test_db.db)
            .await
            .unwrap();
        
        assert!(users_with_posts.len() >= 2);
        
        // Verify that posts can be accessed
        for user in users_with_posts {
            let posts = user.traverse::<Post>(&test_db.db, "posts").await.unwrap();
            // Each user should have at least one post from test data
            assert!(!posts.is_empty());
        }
    }
}

Many-to-Many Relationships

#[cfg(test)]
mod many_to_many_tests {
    use super::*;
    use crate::common::*;
    
    #[tokio::test]
    async fn test_many_to_many_associations() {
        let test_db = TestDatabase::new().await;
        
        let user = UserBuilder::new().save(&test_db.db).await.unwrap();
        let post = PostBuilder::for_user(user.id).save(&test_db.db).await.unwrap();
        
        let tech_category = Category::create()
            .set_name("Technology".to_string())
            .save(&test_db.db)
            .await
            .unwrap();
        
        let programming_category = Category::create()
            .set_name("Programming".to_string())
            .save(&test_db.db)
            .await
            .unwrap();
        
        // Associate post with categories
        PostCategory::create()
            .set_post_id(post.id)
            .set_category_id(tech_category.id)
            .set_created_at(Utc::now())
            .save(&test_db.db)
            .await
            .unwrap();
        
        PostCategory::create()
            .set_post_id(post.id)
            .set_category_id(programming_category.id)
            .set_created_at(Utc::now())
            .save(&test_db.db)
            .await
            .unwrap();
        
        // Test traversal from post to categories
        let categories = post.traverse::<Category>(&test_db.db, "categories").await.unwrap();
        assert_eq!(categories.len(), 2);
        
        let category_names: Vec<_> = categories.iter().map(|c| &c.name).collect();
        assert!(category_names.contains(&&"Technology".to_string()));
        assert!(category_names.contains(&&"Programming".to_string()));
        
        // Test reverse traversal from category to posts
        let posts = tech_category.traverse::<Post>(&test_db.db, "posts").await.unwrap();
        assert_eq!(posts.len(), 1);
        assert_eq!(posts[0].id, post.id);
    }
    
    #[tokio::test]
    async fn test_junction_table_operations() {
        let test_db = TestDatabase::new().await;
        
        let user = UserBuilder::new().save(&test_db.db).await.unwrap();
        let post = PostBuilder::for_user(user.id).save(&test_db.db).await.unwrap();
        let category = Category::create()
            .set_name("Test Category".to_string())
            .save(&test_db.db)
            .await
            .unwrap();
        
        // Test association creation
        let association = PostCategory::create()
            .set_post_id(post.id)
            .set_category_id(category.id)
            .set_created_at(Utc::now())
            .save(&test_db.db)
            .await
            .unwrap();
        
        assert_eq!(association.post_id, post.id);
        assert_eq!(association.category_id, category.id);
        
        // Test finding associations
        let found_associations = PostCategory::query()
            .where_post_id_eq(post.id)
            .all(&test_db.db)
            .await
            .unwrap();
        
        assert_eq!(found_associations.len(), 1);
        
        // Test association deletion
        PostCategory::delete(&test_db.db, association.id).await.unwrap();
        
        let remaining_associations = PostCategory::query()
            .where_post_id_eq(post.id)
            .all(&test_db.db)
            .await
            .unwrap();
        
        assert_eq!(remaining_associations.len(), 0);
    }
}

Integration Testing

Complete Workflow Tests

Test entire user workflows:

#[cfg(test)]
mod integration_tests {
    use super::*;
    use crate::common::*;
    
    #[tokio::test]
    async fn test_blog_workflow() {
        let test_db = TestDatabase::new().await;
        
        // 1. Create a user
        let author = UserBuilder::new()
            .name("Jane Author")
            .email("jane@blogsite.com")
            .save(&test_db.db)
            .await
            .unwrap();
        
        // 2. Create categories
        let tech_category = Category::create()
            .set_name("Technology".to_string())
            .set_description(Some("Tech articles".to_string()))
            .save(&test_db.db)
            .await
            .unwrap();
        
        let tutorial_category = Category::create()
            .set_name("Tutorials".to_string())
            .save(&test_db.db)
            .await
            .unwrap();
        
        // 3. Author creates a draft post
        let draft_post = PostBuilder::for_user(author.id)
            .title("Learning Rust: A Beginner's Guide")
            .content("Rust is a systems programming language...")
            .save(&test_db.db)
            .await
            .unwrap();
        
        assert!(!draft_post.is_published);
        
        // 4. Associate post with categories
        PostCategory::create()
            .set_post_id(draft_post.id)
            .set_category_id(tech_category.id)
            .set_created_at(Utc::now())
            .save(&test_db.db)
            .await
            .unwrap();
        
        PostCategory::create()
            .set_post_id(draft_post.id)
            .set_category_id(tutorial_category.id)
            .set_created_at(Utc::now())
            .save(&test_db.db)
            .await
            .unwrap();
        
        // 5. Publish the post
        let published_post = Post::update(draft_post.id)
            .set_is_published(true)
            .save(&test_db.db)
            .await
            .unwrap();
        
        assert!(published_post.is_published);
        
        // 6. Verify author's published posts
        let author_published = Post::query()
            .where_user_id_eq(author.id)
            .where_is_published_eq(true)
            .all(&test_db.db)
            .await
            .unwrap();
        
        assert_eq!(author_published.len(), 1);
        
        // 7. Verify post categories
        let post_categories = published_post
            .traverse::<Category>(&test_db.db, "categories")
            .await
            .unwrap();
        
        assert_eq!(post_categories.len(), 2);
        
        // 8. Verify category posts
        let tech_posts = tech_category
            .traverse::<Post>(&test_db.db, "posts")
            .await
            .unwrap();
        
        assert_eq!(tech_posts.len(), 1);
        assert_eq!(tech_posts[0].id, published_post.id);
    }
    
    #[tokio::test]
    async fn test_user_content_management() {
        let test_db = TestDatabase::new().await;
        
        let user = UserBuilder::new().save(&test_db.db).await.unwrap();
        
        // Create multiple posts
        let published_count = 3;
        let draft_count = 2;
        
        for i in 1..=published_count {
            PostBuilder::for_user(user.id)
                .title(&format!("Published Post {}", i))
                .published()
                .save(&test_db.db)
                .await
                .unwrap();
        }
        
        for i in 1..=draft_count {
            PostBuilder::for_user(user.id)
                .title(&format!("Draft Post {}", i))
                .save(&test_db.db)
                .await
                .unwrap();
        }
        
        // Test content statistics
        let all_posts = user.traverse::<Post>(&test_db.db, "posts").await.unwrap();
        assert_eq!(all_posts.len(), (published_count + draft_count) as usize);
        
        let published_posts = Post::query()
            .where_user_id_eq(user.id)
            .where_is_published_eq(true)
            .all(&test_db.db)
            .await
            .unwrap();
        assert_eq!(published_posts.len(), published_count as usize);
        
        let draft_posts = Post::query()
            .where_user_id_eq(user.id)
            .where_is_published_eq(false)
            .all(&test_db.db)
            .await
            .unwrap();
        assert_eq!(draft_posts.len(), draft_count as usize);
    }
}

Performance Testing

Load Testing

Test performance with larger datasets:

#[cfg(test)]
mod performance_tests {
    use super::*;
    use crate::common::*;
    use std::time::Instant;
    
    #[tokio::test]
    async fn test_bulk_operations_performance() {
        let test_db = TestDatabase::new().await;
        
        let start = Instant::now();
        
        // Create 1000 users
        for i in 1..=1000 {
            UserBuilder::new()
                .name(&format!("User {}", i))
                .email(&format!("user{}@perf-test.com", i))
                .save(&test_db.db)
                .await
                .unwrap();
        }
        
        let creation_time = start.elapsed();
        println!("Created 1000 users in {:?}", creation_time);
        
        // Query performance test
        let query_start = Instant::now();
        
        let active_users = User::query()
            .where_is_active_eq(true)
            .order_by_name_asc()
            .limit(50)
            .all(&test_db.db)
            .await
            .unwrap();
        
        let query_time = query_start.elapsed();
        println!("Queried 50 users from 1000 in {:?}", query_time);
        
        assert_eq!(active_users.len(), 50);
        
        // Ensure reasonable performance (adjust thresholds as needed)
        assert!(creation_time.as_millis() < 5000); // 5 seconds
        assert!(query_time.as_millis() < 100); // 100ms
    }
    
    #[tokio::test]
    async fn test_relationship_traversal_performance() {
        let test_db = TestDatabase::new().await;
        
        // Create users and posts
        let mut users = Vec::new();
        for i in 1..=10 {
            let user = UserBuilder::new()
                .name(&format!("User {}", i))
                .email(&format!("user{}@test.com", i))
                .save(&test_db.db)
                .await
                .unwrap();
            users.push(user);
        }
        
        // Create 10 posts per user
        for user in &users {
            for j in 1..=10 {
                PostBuilder::for_user(user.id)
                    .title(&format!("Post {} by {}", j, user.name))
                    .published()
                    .save(&test_db.db)
                    .await
                    .unwrap();
            }
        }
        
        // Test traversal performance
        let start = Instant::now();
        
        for user in &users {
            let posts = user.traverse::<Post>(&test_db.db, "posts").await.unwrap();
            assert_eq!(posts.len(), 10);
        }
        
        let traversal_time = start.elapsed();
        println!("Traversed relationships for 10 users with 10 posts each in {:?}", traversal_time);
        
        // Should complete reasonably quickly
        assert!(traversal_time.as_millis() < 1000); // 1 second
    }
}

Memory Usage Testing

Test memory efficiency:

#[cfg(test)]
mod memory_tests {
    use super::*;
    use crate::common::*;
    
    #[tokio::test]
    async fn test_large_result_set_handling() {
        let test_db = TestDatabase::new().await;
        
        // Create a large number of posts
        let user = UserBuilder::new().save(&test_db.db).await.unwrap();
        
        for i in 1..=1000 {
            PostBuilder::for_user(user.id)
                .title(&format!("Post {}", i))
                .content(&format!("Content for post number {}", i))
                .save(&test_db.db)
                .await
                .unwrap();
        }
        
        // Test pagination to avoid loading all at once
        let page_size = 50;
        let mut total_loaded = 0;
        let mut page = 0;
        
        loop {
            let posts = Post::query()
                .where_user_id_eq(user.id)
                .order_by_id_asc()
                .limit(page_size)
                .offset(page * page_size)
                .all(&test_db.db)
                .await
                .unwrap();
            
            if posts.is_empty() {
                break;
            }
            
            total_loaded += posts.len();
            page += 1;
            
            // Verify we're not loading too much at once
            assert!(posts.len() <= page_size as usize);
        }
        
        assert_eq!(total_loaded, 1000);
    }
}

Error Handling Tests

Test error scenarios:

#[cfg(test)]
mod error_tests {
    use super::*;
    use crate::common::*;
    
    #[tokio::test]
    async fn test_constraint_violations() {
        let test_db = TestDatabase::new().await;
        
        // Test unique constraint violation
        UserBuilder::new()
            .email("duplicate@test.com")
            .save(&test_db.db)
            .await
            .unwrap();
        
        let result = UserBuilder::new()
            .email("duplicate@test.com")
            .save(&test_db.db)
            .await;
        
        assert!(result.is_err());
    }
    
    #[tokio::test]
    async fn test_not_found_scenarios() {
        let test_db = TestDatabase::new().await;
        
        // Test finding non-existent user
        let result = User::find(&test_db.db, 999999).await.unwrap();
        assert!(result.is_none());
        
        // Test updating non-existent user
        let result = User::update(999999)
            .set_name("Updated".to_string())
            .save(&test_db.db)
            .await;
        
        assert!(result.is_err());
        
        // Test deleting non-existent user
        let result = User::delete(&test_db.db, 999999).await;
        // Note: Delete might succeed even if record doesn't exist
        // depending on SQLite behavior
    }
    
    #[tokio::test]
    async fn test_foreign_key_constraints() {
        let test_db = TestDatabase::new().await;
        
        // Try to create post with non-existent user
        let result = PostBuilder::for_user(999999)
            .save(&test_db.db)
            .await;
        
        // This should succeed in current implementation
        // but would fail with proper foreign key constraints
        // When FK constraints are enforced, uncomment:
        // assert!(result.is_err());
        
        assert!(result.is_ok()); // Current behavior
    }
}

Test Organization

Test Configuration

// tests/lib.rs
mod common;

mod unit {
    mod entity_tests;
    mod query_tests;
    mod boolean_conversion_tests;
}

mod integration {
    mod relationship_tests;
    mod workflow_tests;
    mod migration_tests;
}

mod performance {
    mod load_tests;
    mod memory_tests;
    mod query_performance_tests;
}

#[cfg(test)]
mod test_config {
    use super::*;
    
    // Global test setup
    fn init_test_logging() {
        // Initialize logging for tests
        env_logger::init();
    }
    
    // Test environment validation
    #[tokio::test]
    async fn test_environment_setup() {
        let db = d1_rs::D1Client::new_in_memory().await.unwrap();
        
        // Verify we can create tables
        let migration = d1_rs::SchemaMigration::new("test_setup".to_string())
            .create_table("test_table")
                .integer("id").primary_key().auto_increment().build()
                .text("name").not_null().build()
            .build();
        
        let result = migration.execute(&db).await;
        assert!(result.is_ok());
    }
}

Continuous Integration

GitHub Actions Configuration

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [ main, dev ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Install Rust
      uses: actions-rust-lang/setup-rust-toolchain@v1
      with:
        toolchain: stable
    
    - name: Run tests
      run: cargo test --verbose
    
    - name: Run tests with all features
      run: cargo test --verbose --all-features
    
    - name: Run performance tests
      run: cargo test --verbose --release performance_tests
    
    - name: Check test coverage
      run: |
        cargo install cargo-tarpaulin
        cargo tarpaulin --verbose --all-features --timeout 120

Best Practices

Test Organization

  1. Separate unit and integration tests
  2. Use descriptive test names
  3. Create reusable test utilities
  4. Test error scenarios
  5. Include performance tests

Test Data Management

  1. Use builders for flexible test data creation
  2. Clean slate for each test (in-memory DB)
  3. Create minimal test data
  4. Test with realistic data volumes

Assertions

  1. Test both positive and negative cases
  2. Verify side effects
  3. Check relationship consistency
  4. Validate data integrity

Next Steps

Performance Optimization

This guide covers strategies for optimizing d1-rs applications for high performance, both in Cloudflare Workers and local development.

Database Design

Indexing Strategy

Proper indexing is crucial for query performance:

// Migration with strategic indexes
let migration = SchemaMigration::new("optimize_performance".to_string())
    .create_table("users")
        .integer("id").primary_key().auto_increment().build()
        .text("email").not_null().unique().build()
        .text("name").not_null().build()
        .boolean("is_active").default_value(DefaultValue::Boolean(true)).build()
        .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .datetime("last_login").build()
    .build()
    
    // Add strategic indexes
    .alter_table("users")
        // Single column indexes
        .add_index("idx_users_email", vec!["email"])
        .add_index("idx_users_active", vec!["is_active"])
        .add_index("idx_users_created", vec!["created_at"])
        .add_index("idx_users_last_login", vec!["last_login"])
        
        // Composite indexes for common query patterns
        .add_index("idx_users_active_created", vec!["is_active", "created_at"])
        .add_index("idx_users_active_name", vec!["is_active", "name"])
    .build()
    
    .create_table("posts")
        .integer("id").primary_key().auto_increment().build()
        .integer("user_id").not_null().build()
        .text("title").not_null().build()
        .text("content").not_null().build()
        .boolean("is_published").default_value(DefaultValue::Boolean(false)).build()
        .datetime("created_at").default_value(DefaultValue::CurrentTimestamp).build()
        .datetime("published_at").build()
    .build()
    
    .alter_table("posts")
        // Foreign key index
        .add_index("idx_posts_user_id", vec!["user_id"])
        
        // Query-specific indexes
        .add_index("idx_posts_published", vec!["is_published"])
        .add_index("idx_posts_published_at", vec!["published_at"])
        
        // Composite indexes for common queries
        .add_index("idx_posts_user_published", vec!["user_id", "is_published"])
        .add_index("idx_posts_published_created", vec!["is_published", "created_at"])
    .build();

Denormalization Strategies

Strategic denormalization can improve read performance:

#[derive(Entity, RelationalEntity)]
pub struct OptimizedUser {
    #[primary_key]
    pub id: i64,
    pub name: String,
    pub email: String,
    pub is_active: bool,
    
    // Denormalized counters
    pub post_count: i64,
    pub published_post_count: i64,
    pub last_post_date: Option<DateTime<Utc>>,
    
    // Cached derived data
    pub activity_score: f64, // Calculated based on posts, comments, etc.
    pub stats_updated_at: DateTime<Utc>,
}

impl OptimizedUser {
    // Update denormalized data
    pub async fn refresh_stats(&self, db: &D1Client) -> Result<OptimizedUser> {
        let posts = self.traverse::<Post>(db, "posts").await?;
        let published_posts: Vec<_> = posts.iter()
            .filter(|p| p.is_published)
            .collect();
        
        let last_post_date = posts.iter()
            .map(|p| p.created_at)
            .max();
        
        // Calculate activity score (example algorithm)
        let activity_score = self.calculate_activity_score(&posts).await;
        
        OptimizedUser::update(self.id)
            .set_post_count(posts.len() as i64)
            .set_published_post_count(published_posts.len() as i64)
            .set_last_post_date(last_post_date)
            .set_activity_score(activity_score)
            .set_stats_updated_at(Utc::now())
            .save(db)
            .await
    }
    
    async fn calculate_activity_score(&self, posts: &[Post]) -> f64 {
        // Example: Score based on recency and volume
        let now = Utc::now();
        let mut score = 0.0;
        
        for post in posts {
            let days_old = (now - post.created_at).num_days() as f64;
            let recency_factor = 1.0 / (1.0 + days_old / 30.0); // Decay over 30 days
            
            if post.is_published {
                score += 2.0 * recency_factor;
            } else {
                score += 1.0 * recency_factor;
            }
        }
        
        score
    }
    
    // Fast queries using denormalized data
    pub async fn most_active_users(db: &D1Client, limit: i64) -> Result<Vec<OptimizedUser>> {
        OptimizedUser::query()
            .where_is_active_eq(true)
            .order_by_activity_score_desc()
            .limit(limit)
            .all(db)
            .await
    }
    
    pub async fn recently_active_users(db: &D1Client, days: i64) -> Result<Vec<OptimizedUser>> {
        let cutoff = Utc::now() - Duration::days(days);
        
        OptimizedUser::query()
            .where_last_post_date_gte(cutoff)
            .order_by_last_post_date_desc()
            .all(db)
            .await
    }
}

Query Optimization

Efficient Query Patterns

Write queries that minimize database work:

// Good: Specific queries
impl Post {
    pub async fn recent_published(db: &D1Client, days: i64, limit: i64) -> Result<Vec<Post>> {
        let cutoff = Utc::now() - Duration::days(days);
        
        Post::query()
            .where_is_published_eq(true)
            .where_created_at_gte(cutoff)
            .order_by_created_at_desc()
            .limit(limit)
            .all(db)
            .await
    }
    
    // Avoid: Loading unnecessary data
    // pub async fn all_posts_then_filter() -> Result<Vec<Post>> {
    //     let all_posts = Post::query().all(db).await?;
    //     let filtered: Vec<_> = all_posts.into_iter()
    //         .filter(|p| p.is_published)
    //         .take(10)
    //         .collect();
    //     Ok(filtered)
    // }
}

Pagination Best Practices

Implement efficient pagination:

#[derive(Debug, Serialize)]
pub struct PaginatedResult<T> {
    pub data: Vec<T>,
    pub page: i64,
    pub per_page: i64,
    pub total_count: Option<i64>,
    pub has_next: bool,
}

impl<T> PaginatedResult<T> {
    pub fn new(data: Vec<T>, page: i64, per_page: i64, has_next: bool) -> Self {
        Self {
            data,
            page,
            per_page,
            total_count: None,
            has_next,
        }
    }
    
    pub fn with_total_count(mut self, total_count: i64) -> Self {
        self.total_count = Some(total_count);
        self
    }
}

impl Post {
    pub async fn paginated(
        db: &D1Client,
        page: i64,
        per_page: i64,
        include_total: bool,
    ) -> Result<PaginatedResult<Post>> {
        let offset = page * per_page;
        
        // Load one extra to check if there's a next page
        let posts = Post::query()
            .where_is_published_eq(true)
            .order_by_created_at_desc()
            .limit(per_page + 1)
            .offset(offset)
            .all(db)
            .await?;
        
        let has_next = posts.len() > per_page as usize;
        let mut data = posts;
        if has_next {
            data.pop(); // Remove the extra item
        }
        
        let mut result = PaginatedResult::new(data, page, per_page, has_next);
        
        if include_total {
            let total_count = Post::query()
                .where_is_published_eq(true)
                .count(db)
                .await?;
            result = result.with_total_count(total_count);
        }
        
        Ok(result)
    }
    
    // Cursor-based pagination (more efficient for large datasets)
    pub async fn paginated_cursor(
        db: &D1Client,
        cursor: Option<i64>, // Last seen ID
        limit: i64,
    ) -> Result<PaginatedResult<Post>> {
        let mut query = Post::query()
            .where_is_published_eq(true)
            .order_by_id_desc()
            .limit(limit + 1);
        
        if let Some(cursor_id) = cursor {
            query = query.where_id_lt(cursor_id);
        }
        
        let posts = query.all(db).await?;
        
        let has_next = posts.len() > limit as usize;
        let mut data = posts;
        if has_next {
            data.pop();
        }
        
        Ok(PaginatedResult::new(data, 0, limit, has_next))
    }
}

Batch Operations

Optimize bulk operations:

impl User {
    // Efficient bulk user creation
    pub async fn create_bulk(db: &D1Client, user_data: Vec<CreateUserData>) -> Result<Vec<User>> {
        let mut created_users = Vec::new();
        
        // In a real implementation with transactions:
        // db.transaction(|tx| async {
        //     for data in user_data {
        //         let user = User::create()
        //             .set_name(data.name)
        //             .set_email(data.email)
        //             .save(&tx)
        //             .await?;
        //         created_users.push(user);
        //     }
        //     Ok(created_users)
        // }).await
        
        // Current implementation
        for data in user_data {
            let user = User::create()
                .set_name(data.name)
                .set_email(data.email)
                .set_is_active(true)
                .save(db)
                .await?;
            created_users.push(user);
        }
        
        Ok(created_users)
    }
    
    // Efficient bulk updates
    pub async fn activate_users_bulk(db: &D1Client, user_ids: Vec<i64>) -> Result<Vec<User>> {
        let mut updated_users = Vec::new();
        
        for user_id in user_ids {
            let updated = User::update(user_id)
                .set_is_active(true)
                .save(db)
                .await?;
            updated_users.push(updated);
        }
        
        Ok(updated_users)
    }
}

#[derive(Debug)]
pub struct CreateUserData {
    pub name: String,
    pub email: String,
}

Memory Management

Lazy Loading

Avoid loading unnecessary data:

impl User {
    // Lazy-loaded relationships
    pub async fn recent_posts_lazy(&self, db: &D1Client, limit: i64) -> Result<Vec<Post>> {
        // Only load what we need
        Post::query()
            .where_user_id_eq(self.id)
            .order_by_created_at_desc()
            .limit(limit)
            .all(db)
            .await
    }
    
    // Avoid: Eager loading everything
    // pub async fn all_data_eager(&self, db: &D1Client) -> Result<UserWithEverything> {
    //     let posts = self.traverse::<Post>(db, "posts").await?;
    //     let profile = self.traverse::<Profile>(db, "profile").await?;
    //     let comments = self.traverse::<Comment>(db, "comments").await?;
    //     // This loads potentially massive amounts of data
    // }
}

Selective Field Loading

Load only required fields (future feature):

// This pattern will be supported in future versions
// #[derive(Debug, Serialize, Deserialize)]
// pub struct UserSummary {
//     pub id: i64,
//     pub name: String,
//     pub is_active: bool,
// }
// 
// impl User {
//     pub async fn summaries(db: &D1Client, limit: i64) -> Result<Vec<UserSummary>> {
//         // Future: SELECT id, name, is_active FROM users LIMIT ?
//         // For now: Load full entities and transform
//         let users = User::query().limit(limit).all(db).await?;
//         let summaries = users.into_iter().map(|u| UserSummary {
//             id: u.id,
//             name: u.name,
//             is_active: u.is_active,
//         }).collect();
//         Ok(summaries)
//     }
// }

Cloudflare Workers Optimization

Edge-Specific Patterns

Optimize for the Workers runtime:

use worker::*;

// Efficient handler pattern
#[event(fetch)]
async fn main(req: Request, env: Env, ctx: worker::Context) -> Result<Response> {
    let db = D1Client::new(env.d1("DB")?);
    
    match req.method() {
        Method::Get => {
            match req.path().as_str() {
                "/users" => handle_get_users(&db, &req).await,
                "/posts" => handle_get_posts(&db, &req).await,
                path if path.starts_with("/users/") => handle_get_user(&db, path).await,
                _ => Response::error("Not found", 404),
            }
        }
        Method::Post => {
            match req.path().as_str() {
                "/users" => handle_create_user(&db, req).await,
                "/posts" => handle_create_post(&db, req).await,
                _ => Response::error("Not found", 404),
            }
        }
        _ => Response::error("Method not allowed", 405),
    }
}

async fn handle_get_users(db: &D1Client, req: &Request) -> Result<Response> {
    // Parse query parameters for pagination
    let url = req.url()?;
    let page = url.search_params()
        .get("page")
        .and_then(|p| p.parse::<i64>().ok())
        .unwrap_or(0);
    let per_page = url.search_params()
        .get("per_page")
        .and_then(|p| p.parse::<i64>().ok())
        .unwrap_or(10)
        .min(100); // Limit max page size
    
    // Use efficient pagination
    let result = User::paginated(db, page, per_page, false).await
        .map_err(|e| worker::Error::RustError(format!("Database error: {}", e)))?;
    
    Response::from_json(&result)
}

async fn handle_get_posts(db: &D1Client, req: &Request) -> Result<Response> {
    let url = req.url()?;
    
    // Check for cursor-based pagination
    if let Some(cursor) = url.search_params().get("cursor") {
        let cursor_id = cursor.parse::<i64>()
            .map_err(|_| worker::Error::BadRequest)?;
        
        let limit = url.search_params()
            .get("limit")
            .and_then(|l| l.parse::<i64>().ok())
            .unwrap_or(20)
            .min(100);
        
        let result = Post::paginated_cursor(db, Some(cursor_id), limit).await
            .map_err(|e| worker::Error::RustError(format!("Database error: {}", e)))?;
        
        Response::from_json(&result)
    } else {
        // Regular pagination
        let page = url.search_params()
            .get("page")
            .and_then(|p| p.parse::<i64>().ok())
            .unwrap_or(0);
        
        let result = Post::paginated(db, page, 20, false).await
            .map_err(|e| worker::Error::RustError(format!("Database error: {}", e)))?;
        
        Response::from_json(&result)
    }
}

Response Caching

Cache responses at the edge:

async fn handle_popular_posts(db: &D1Client, ctx: &worker::Context) -> Result<Response> {
    let cache = ctx.env.cf().unwrap().cache();
    let cache_key = "popular_posts_v1";
    
    // Try cache first
    if let Some(cached_response) = cache.get(cache_key, CacheOptions::default()).await {
        return Ok(cached_response);
    }
    
    // Load from database
    let posts = Post::query()
        .where_is_published_eq(true)
        .order_by_created_at_desc()
        .limit(10)
        .all(db)
        .await
        .map_err(|e| worker::Error::RustError(format!("Database error: {}", e)))?;
    
    let response = Response::from_json(&posts)?;
    
    // Cache for 5 minutes
    cache.put(cache_key, response.clone(), CacheTtl::Minutes(5)).await;
    
    Ok(response)
}

Monitoring and Profiling

Performance Metrics

Track key performance indicators:

use std::time::Instant;

pub struct QueryMetrics {
    pub query_time: std::time::Duration,
    pub result_count: usize,
    pub query_type: String,
}

impl User {
    pub async fn find_with_metrics(db: &D1Client, id: i64) -> Result<(Option<User>, QueryMetrics)> {
        let start = Instant::now();
        
        let user = User::find(db, id).await?;
        
        let metrics = QueryMetrics {
            query_time: start.elapsed(),
            result_count: if user.is_some() { 1 } else { 0 },
            query_type: "find_user".to_string(),
        };
        
        // Log metrics (in Workers, use console.log)
        console_log!(
            "Query: {} took {:?} and returned {} results",
            metrics.query_type,
            metrics.query_time,
            metrics.result_count
        );
        
        Ok((user, metrics))
    }
}

// Middleware pattern for automatic metrics
pub async fn with_metrics<F, T>(
    operation_name: &str,
    operation: F,
) -> Result<(T, QueryMetrics)>
where
    F: std::future::Future<Output = Result<T>>,
    T: std::fmt::Debug,
{
    let start = Instant::now();
    let result = operation.await?;
    
    let metrics = QueryMetrics {
        query_time: start.elapsed(),
        result_count: 1, // Would need to be calculated based on T
        query_type: operation_name.to_string(),
    };
    
    console_log!("Operation {} completed in {:?}", operation_name, metrics.query_time);
    
    Ok((result, metrics))
}

Query Analysis

Analyze and optimize slow queries:

pub struct SlowQueryLogger;

impl SlowQueryLogger {
    pub fn log_if_slow(query_name: &str, duration: std::time::Duration, threshold_ms: u64) {
        if duration.as_millis() > threshold_ms as u128 {
            console_log!(
                "SLOW QUERY: {} took {}ms (threshold: {}ms)",
                query_name,
                duration.as_millis(),
                threshold_ms
            );
        }
    }
}

impl User {
    pub async fn complex_query_with_logging(db: &D1Client) -> Result<Vec<User>> {
        let start = Instant::now();
        
        let users = User::query()
            .where_is_active_eq(true)
            .order_by_name_asc()
            .all(db)
            .await?;
        
        SlowQueryLogger::log_if_slow("complex_user_query", start.elapsed(), 100);
        
        Ok(users)
    }
}

Best Practices Summary

Do's

  1. Index frequently queried columns
  2. Use pagination for large result sets
  3. Load only necessary data
  4. Batch operations when possible
  5. Cache expensive computations
  6. Monitor query performance

Don'ts

  1. Don't load all data then filter in Rust
  2. Don't ignore database indexes
  3. Don't perform N+1 queries
  4. Don't load unnecessary relationship data
  5. Don't forget to limit query results

Cloudflare Workers Specific

  1. Use edge caching for static data
  2. Implement request coalescing for similar queries
  3. Monitor cold start times
  4. Optimize bundle size

Next Steps

Blog System

E-commerce

User Management

Cloudflare Workers

Local Development

CI/CD Setup

API Reference

Migration Reference

Configuration

Troubleshooting