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
Feature | d1-rs | Rails/ActiveRecord | Django ORM | Eloquent | Ent-Go | Prisma |
---|---|---|---|---|---|---|
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
Feature | Description | When to Use |
---|---|---|
sqlite | Enable SQLite backend | Local development, testing |
d1 | Enable Cloudflare D1 backend | Production 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:
- Revolutionary Entity Definition: Zero-boilerplate
#[derive(Entity)]
with perfect type safety - World's First Type-Safe Relations:
relations!
macro with zero string literals, compile-time validation - Effortless Schema Migration: Automatic table creation with
SchemaMigration
- Impossible-to-Match Association Methods: Generated methods with perfect IDE auto-completion
- Superior CRUD Operations: Fluent API that eliminates all runtime errors
- Compile-Time Safe Queries: Methods like
where_is_active_eq()
validated at compile-time - World's First Nested Eager Loading: Automatic N+1 prevention with compile-time safety
- 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:
- Revolutionary Relations: Nested eager loading, recursive relationships, rich M2M junction entities
- Advanced Revolutionary Features: Compile-time safe complex queries impossible in other ORMs
- Performance Superiority: Learn why d1-rs outperforms every other ORM
- Zero-Config Deployment: Deploy to Cloudflare Workers with perfect type safety
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 operationsSerialize, 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:
User
→users
BlogPost
→blog_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'sINTEGER
(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:
- Queries - Advanced querying techniques
- Relations - Connecting entities together
- Migrations - Managing database schema changes
- Boolean Handling - Deep dive into boolean field conversion
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'sINTEGER
(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:
User
→users
BlogPost
→blog_posts
Category
→categories
(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
- Learn about Relations for joining data across tables
- Explore Boolean Handling for understanding boolean conversion
- Check out Performance Optimization for query tuning tips
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 Type | SQLite Type | Rust Type | Description |
---|---|---|---|
integer() | INTEGER | i64 | 64-bit signed integer |
text() | TEXT | String | UTF-8 string |
real() | REAL | f64 | Floating point number |
boolean() | INTEGER | bool | Boolean (stored as 0/1) |
datetime() | DATETIME | DateTime<Utc> | ISO 8601 datetime |
json() | TEXT | serde_json::Value | JSON 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 recordsSetNull
- Set foreign key to NULLSetDefault
- Set foreign key to default valueRestrict
- Prevent the action if child records existNoAction
- 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:
- One-to-One Relations - User profiles, account settings
- One-to-Many Relations - Users and posts, categories and items
- Many-to-Many Relations - Posts and tags, users and roles
- Advanced Relations - Complex patterns and performance optimization
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 Related Records
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
- Learn about One-to-Many Relations for parent-child relationships
- Explore Many-to-Many Relations for complex associations
- Check out Advanced Relations for complex traversal patterns
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 Related Records
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)
}
}
Bulk Operations on Related Records
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
- Learn about Many-to-Many Relations for complex junction table relationships
- Explore Advanced Relations for complex patterns and performance optimization
- Check out One-to-One Relations for unique associations
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
- Learn about Advanced Relations for complex graph traversal and optimization
- Explore Performance Optimization for scaling many-to-many relationships
- Check out Testing Relations for comprehensive relationship testing
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:
true
→1
false
→0
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
- Use descriptive names:
is_active
instead ofactive
- Default to safe values: Usually
false
for permissions,true
for enabled features - Consider three-state logic: Use
Option<bool>
when "unknown" is meaningful - Group related booleans: Consider separate settings tables for complex boolean groups
Query Optimization
- Index frequently queried boolean fields
- Use compound indexes for common boolean combinations
- Structure WHERE clauses to use indexes effectively
- 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
- Learn about Performance Optimization for boolean field indexing strategies
- Explore Testing Strategies for comprehensive boolean testing
- Check out Migrations for boolean field evolution patterns
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
- Separate unit and integration tests
- Use descriptive test names
- Create reusable test utilities
- Test error scenarios
- Include performance tests
Test Data Management
- Use builders for flexible test data creation
- Clean slate for each test (in-memory DB)
- Create minimal test data
- Test with realistic data volumes
Assertions
- Test both positive and negative cases
- Verify side effects
- Check relationship consistency
- Validate data integrity
Next Steps
- Learn about Boolean Handling for type conversion testing
- Explore Performance Optimization for production readiness
- Check out Schema Evolution for migration testing
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
- Index frequently queried columns
- Use pagination for large result sets
- Load only necessary data
- Batch operations when possible
- Cache expensive computations
- Monitor query performance
Don'ts
- Don't load all data then filter in Rust
- Don't ignore database indexes
- Don't perform N+1 queries
- Don't load unnecessary relationship data
- Don't forget to limit query results
Cloudflare Workers Specific
- Use edge caching for static data
- Implement request coalescing for similar queries
- Monitor cold start times
- Optimize bundle size
Next Steps
- Learn about Testing Strategies for performance testing
- Explore Boolean Handling for type conversion optimization
- Check out Migrations for evolving schemas efficiently