🌊 TideORM

A Developer-Friendly, Production-Ready ORM for Rust

✅ v0.4.5 PostgreSQL • MySQL • SQLite

📚 Documentation v0.4.5

Complete documentation for TideORM - a developer-friendly ORM for Rust.

🎯 Overview

TideORM is a modern, type-safe ORM for Rust inspired by Laravel Eloquent, Rails ActiveRecord, and built on SeaORM. It provides a clean, intuitive API for database operations while maintaining full Rust type safety.

Why TideORM?

🔒 Type Safe

Compile-time guarantees for your database operations. No runtime surprises.

⚡ Async First

Built on async/await from the ground up. Perfect for high-concurrency apps.

🌐 Multi-Database

Single API works with PostgreSQL, MySQL, and SQLite.

🎨 Clean Syntax

Fluent query builder with chainable methods. Readable and maintainable.

📊 SeaORM 2.0

Full support for SeaORM 2.0 features: CTEs, window functions, and more.

🛠️ Developer Tools

Query logging, validation, file attachments, and i18n built-in.

What's New in v0.4.5

📦 Installation

Requirements

Add to Cargo.toml

[dependencies]
tideorm = "0.4"
tokio = { version = "1.37", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }

Note: TideORM requires tokio for async runtime. Add serde and chrono for serialization and timestamps.

Feature Flags

FeatureDescriptionDefault
postgresPostgreSQL support
mysqlMySQL/MariaDB support
sqliteSQLite support
runtime-tokioTokio async runtime
runtime-async-stdasync-std runtime

Using Multiple Databases

[dependencies]
tideorm = { version = "0.4", features = ["postgres", "mysql", "sqlite"] }

🚀 Quick Start

Step 1: Connect to Database

use tideorm::prelude::*;

#[tokio::main]
async fn main() -> tideorm::Result<()> {
    // Simple connection
    Database::init("postgres://localhost/mydb").await?;
    
    // Or with configuration
    TideConfig::init()
        .database("postgres://user:password@localhost/myapp")
        .max_connections(20)
        .min_connections(5)
        .connect()
        .await?;
    
    Ok(())
}

Step 2: Define a Model

use tideorm::prelude::*;

// Auto-derives Debug, Clone, Serialize, Deserialize
#[tideorm::model]
#[tide(table = "users")]
pub struct User {
    #[tide(primary_key, auto_increment)]
    pub id: i64,
    pub name: String,
    pub email: String,
    pub active: bool,
}

Step 3: CRUD Operations

// Create
let user = User {
    id: 0,
    name: "John Doe".to_string(),
    email: "john@example.com".to_string(),
    active: true,
};
let user = user.save().await?;
println!("Created user with id: {}", user.id);

// Read
let user = User::find(1).await?;
let users = User::all().await?;
let active_users = User::query()
    .where_eq("active", true)
    .get()
    .await?;

// Update
let mut user = User::find(1).await?.unwrap();
user.name = "Jane Doe".to_string();
let user = user.update().await?;

// Delete
user.delete().await?;
User::destroy(1).await?;

📋 Model Definition

Default Behavior (Recommended)

The #[tideorm::model] macro automatically implements Debug, Clone, Default, Serialize, and Deserialize:

#[tideorm::model]
#[tide(table = "products")]
#[index("category")]
#[unique_index("sku")]
pub struct Product {
    #[tide(primary_key, auto_increment)]
    pub id: i64,
    pub name: String,
    pub sku: String,
    pub category: String,
    pub price: i64,
    #[tide(nullable)]
    pub description: Option<String>,
    pub active: bool,
}

Custom Implementations

Use skip_derives to provide your own implementations:

#[tideorm::model]
#[tide(table = "products", skip_derives)]
pub struct Product {
    // ... fields ...
}

impl Debug for Product { /* custom impl */ }
impl Clone for Product { /* custom impl */ }

Model Attributes

Struct-LevelDescription
#[tide(table = "name")]Custom table name
#[tide(skip_derives)]Skip all auto-generated traits
#[tide(skip_debug)]Skip Debug impl only
#[tide(soft_delete)]Enable soft deletes
#[index("col")]Create index
#[unique_index("col")]Create unique index
Field-LevelDescription
#[tide(primary_key)]Mark as primary key
#[tide(auto_increment)]Auto-increment field
#[tide(nullable)]Optional/nullable field
#[tide(column = "name")]Custom column name
#[tide(default = "value")]Default value
#[tide(skip)]Skip field in queries

Auto-Timestamps

use chrono::{DateTime, Utc};

#[tideorm::model]
#[tide(table = "posts")]
pub struct Post {
    #[tide(primary_key, auto_increment)]
    pub id: i64,
    pub title: String,
    pub created_at: DateTime<Utc>,  // Auto-set on save()
    pub updated_at: DateTime<Utc>,  // Auto-set on save() and update()
}

// No need to set timestamps manually
let post = Post { title: "Hello".into(), ..Default::default() };
let post = post.save().await?;  // Timestamps automatically set

Soft Deletes

#[tideorm::model]
#[tide(table = "posts", soft_delete)]
pub struct Post {
    #[tide(primary_key, auto_increment)]
    pub id: i64,
    pub title: String,
    pub deleted_at: Option<DateTime<Utc>>,
}

// Soft delete (sets deleted_at)
let post = post.soft_delete().await?;

// Restore
let post = post.restore().await?;

// Query active records (default)
let posts = Post::query().get().await?;

// Include soft-deleted
let all = Post::query().with_trashed().get().await?;

// Only soft-deleted
let trashed = Post::query().only_trashed().get().await?;

🔗 Model Relations

TideORM supports SeaORM-style relations defined as struct fields. Relations are lazy-loaded on demand.

Defining Relations

use tideorm::prelude::*;

#[tideorm::model]
#[tide(table = "users")]
pub struct User {
    #[tide(primary_key, auto_increment)]
    pub id: i64,
    pub name: String,
    
    // One-to-one: User has one Profile
    #[tide(has_one = "Profile", foreign_key = "user_id")]
    pub profile: HasOne<Profile>,
    
    // One-to-many: User has many Posts
    #[tide(has_many = "Post", foreign_key = "user_id")]
    pub posts: HasMany<Post>,
}

#[tideorm::model]
#[tide(table = "profiles")]
pub struct Profile {
    #[tide(primary_key, auto_increment)]
    pub id: i64,
    pub user_id: i64,
    pub bio: String,
    
    // Inverse: Profile belongs to User
    #[tide(belongs_to = "User", foreign_key = "user_id")]
    pub user: BelongsTo<User>,
}

Loading Relations

// Load HasOne relation
let user = User::find(1).await?.unwrap();
let profile: Option<Profile> = user.profile.load().await?;

// Load HasMany relation
let posts: Vec<Post> = user.posts.load().await?;

// Load BelongsTo relation
let post = Post::find(1).await?.unwrap();
let author: Option<User> = post.author.load().await?;

// Check existence
let has_profile = user.profile.exists().await?;  // bool

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

Loading with Constraints

// Load posts with custom conditions
let recent_posts = user.posts.load_with(|query| {
    query
        .where_eq("published", true)
        .where_gt("views", 100)
        .order_desc("created_at")
        .limit(10)
}).await?;

Many-to-Many Relations

#[tideorm::model]
#[tide(table = "users")]
pub struct User {
    #[tide(primary_key, auto_increment)]
    pub id: i64,
    
    // Many-to-many through pivot table
    #[tide(has_many_through = "Role", pivot = "user_roles", 
           foreign_key = "user_id", related_key = "role_id")]
    pub roles: HasManyThrough<Role, UserRole>,
}

// Usage
let user = User::find(1).await?.unwrap();
let roles = user.roles.load().await?;

// Attach a role
user.roles.attach(role_id).await?;

// Detach a role
user.roles.detach(role_id).await?;

// Sync roles (replace all)
user.roles.sync(vec![
    serde_json::json!(1),
    serde_json::json!(2),
]).await?;

Self-Referencing Relations

NEW in v0.4: Support for hierarchical data like org charts or category trees.

#[tideorm::model]
#[tide(table = "employees")]
pub struct Employee {
    #[tide(primary_key)]
    pub id: i64,
    pub name: String,
    pub manager_id: Option<i64>,
    
    // Parent reference (manager)
    #[tide(self_ref = "id", foreign_key = "manager_id")]
    pub manager: SelfRef<Employee>,
    
    // Children reference (direct reports)
    #[tide(self_ref_many = "id", foreign_key = "manager_id")]
    pub reports: SelfRefMany<Employee>,
}

// Load entire subtree recursively
let tree = emp.reports.load_tree(3).await?;  // 3 levels deep

🔍 Query Builder

WHERE Conditions

// Equality
User::query().where_eq("status", "active")
User::query().where_not("role", "admin")

// Comparison
User::query().where_gt("age", 18)
User::query().where_gte("age", 18)
User::query().where_lt("age", 65)
User::query().where_lte("age", 65)

// Pattern matching
User::query().where_like("name", "%John%")
User::query().where_not_like("email", "%spam%")

// IN / NOT IN
User::query().where_in("role", vec!["admin", "moderator"])
User::query().where_not_in("status", vec!["banned"])

// NULL checks
User::query().where_null("deleted_at")
User::query().where_not_null("email")

// Range
User::query().where_between("age", 18, 65)

Strongly-Typed Columns

NEW in v0.4: Compile-time type safety for column operations. The compiler catches type mismatches!

use tideorm::columns::Column;

mod user_columns {
    use tideorm::columns::Column;
    
    pub const ID: Column<i64> = Column::new("id");
    pub const NAME: Column<String> = Column::new("name");
    pub const AGE: Column<Option<i32>> = Column::new("age");
    pub const ACTIVE: Column<bool> = Column::new("active");
}

use user_columns::*;

// Type-safe queries - compiler catches errors!
let users = User::query()
    .where_col(NAME.eq("Alice"))           // ✓ String == &str
    .where_col(NAME.contains("test"))      // ✓ LIKE '%test%'
    .where_col(AGE.gt(18))                 // ✓ Option<i32> > i32
    .where_col(AGE.is_null())              // ✓ Nullable check
    .where_col(ACTIVE.eq(true))            // ✓ bool == bool
    // .where_col(NAME.eq(123))            // ✗ COMPILE ERROR!
    .get()
    .await?;

Ordering & Pagination

// Ordering
User::query()
    .order_by("created_at", Order::Desc)
    .order_asc("name")
    .order_desc("age")
    .latest()  // ORDER BY created_at DESC
    .oldest()  // ORDER BY created_at ASC
    .get()
    .await?;

// Pagination
User::query()
    .limit(10)
    .offset(20)
    .page(3, 25)  // Page 3, 25 per page
    .get()
    .await?;

Execution Methods

// Get all records
let users = User::query().where_eq("active", true).get().await?;

// Get first
let user = User::query()
    .where_eq("email", "admin@example.com")
    .first()
    .await?;

// First or fail
let user = User::query()
    .where_eq("id", 1)
    .first_or_fail()
    .await?;

// Count (efficient SQL COUNT)
let count = User::query()
    .where_eq("active", true)
    .count()
    .await?;

// Check existence
let exists = User::query()
    .where_eq("email", "admin@example.com")
    .exists()
    .await?;

// Bulk delete (single DELETE statement)
let deleted = User::query()
    .where_eq("status", "inactive")
    .delete()
    .await?;

Window Functions

NEW in v0.4: Full support for window functions across all databases!

// ROW_NUMBER - assign sequential numbers
let products = Product::query()
    .row_number("row_num", Some("category"), "price", Order::Desc)
    .get_raw()
    .await?;

// RANK - rank with gaps for ties
let employees = Employee::query()
    .rank("salary_rank", Some("department_id"), "salary", Order::Desc)
    .get_raw()
    .await?;

// DENSE_RANK - rank without gaps
let students = Student::query()
    .dense_rank("score_rank", None, "score", Order::Desc)
    .get_raw()
    .await?;

// Running totals with SUM
let sales = Sale::query()
    .running_sum("running_total", "amount", "date", Order::Asc)
    .get_raw()
    .await?;

// LAG - access previous row value
let orders = Order::query()
    .lag("prev_total", "total", 1, Some("0"), "user_id", "created_at", Order::Asc)
    .get_raw()
    .await?;

// NTILE - distribute into buckets
let products = Product::query()
    .ntile("price_quartile", 4, "price", Order::Asc)
    .get_raw()
    .await?;

CTEs (Common Table Expressions)

NEW in v0.4: Define temporary named result sets for complex queries!

// Simple CTE
let orders = Order::query()
    .with_cte(CTE::new(
        "high_value_orders",
        "SELECT * FROM orders WHERE total > 1000".to_string()
    ))
    .where_raw("id IN (SELECT id FROM high_value_orders)")
    .get()
    .await?;

// CTE from another query builder
let active_users = User::query()
    .where_eq("active", true)
    .select(vec!["id", "name", "email"]);

let posts = Post::query()
    .with_query("active_users", active_users)
    .inner_join("active_users", "posts.user_id", "active_users.id")
    .get()
    .await?;

// Recursive CTE for hierarchical data
let employees = Employee::query()
    .with_recursive_cte(
        "org_tree",
        vec!["id", "name", "manager_id", "level"],
        // Base case: top-level managers
        "SELECT id, name, manager_id, 0 FROM employees WHERE manager_id IS NULL",
        // Recursive: employees under managers
        "SELECT e.id, e.name, e.manager_id, t.level + 1 
         FROM employees e 
         INNER JOIN org_tree t ON e.manager_id = t.id"
    )
    .where_raw("id IN (SELECT id FROM org_tree)")
    .get()
    .await?;

UNION Queries

NEW in v0.4: Combine results from multiple queries!

// UNION - removes duplicates
let users = User::query()
    .where_eq("active", true)
    .union(User::query().where_eq("role", "admin"))
    .get()
    .await?;

// UNION ALL - keeps duplicates (faster)
let orders = Order::query()
    .where_eq("status", "pending")
    .union_all(Order::query().where_eq("status", "processing"))
    .union_all(Order::query().where_eq("status", "shipped"))
    .order_by("created_at", Order::Desc)
    .get()
    .await?;

// Raw UNION for complex queries
let results = User::query()
    .union_raw("SELECT * FROM archived_users WHERE year = 2023")
    .get()
    .await?;

⚙️ Advanced Features

File Attachments

NEW in v0.4: Built-in file attachment system for managing file relationships with metadata!

#[tideorm::model]
#[tide(table = "products")]
#[tide(has_one_file = "thumbnail")]
#[tide(has_many_files = "images,documents")]
pub struct Product {
    #[tide(primary_key, auto_increment)]
    pub id: i64,
    pub name: String,
    pub files: Option<Json>,  // JSONB column storing attachments
}

// Attach files
product.attach("thumbnail", "uploads/thumb.jpg")?;
product.attach_many("images", vec!["img1.jpg", "img2.jpg"])?;

// Attach with metadata
let attachment = FileAttachment::with_metadata(
    "uploads/document.pdf",
    Some("My Document.pdf"),  // Original filename
    Some(1024 * 1024),        // File size (1MB)
    Some("application/pdf"),  // MIME type
);
product.attach_with_metadata("documents", attachment)?;

// Add custom metadata
let attachment = FileAttachment::new("uploads/photo.jpg")
    .add_metadata("width", 1920)
    .add_metadata("height", 1080)
    .add_metadata("photographer", "John Doe");
product.attach_with_metadata("images", attachment)?;

// Get files
if let Some(thumb) = product.get_file("thumbnail")? {
    println!("Thumbnail: {}", thumb.key);
    println!("Filename: {}", thumb.filename);
    println!("Size: {:?}", thumb.size);
}

let images = product.get_files("images")?;
for img in images {
    println!("Image: {} ({})", img.filename, img.key);
}

// Check if has files
if product.has_files("images")? {
    let count = product.count_files("images")?;
    println!("Product has {} images", count);
}

// Detach files
product.detach("images", Some("img1.jpg"))?;  // Remove specific
product.detach("images", None)?;              // Remove all

// Sync files (replace all)
product.sync("images", vec!["new1.jpg", "new2.jpg"])?;

product.update().await?;

Translations (i18n)

NEW in v0.4: Multilingual content support with JSONB storage!

#[tideorm::model]
#[tide(table = "products")]
#[tide(translatable = "name,description")]
pub struct Product {
    #[tide(primary_key, auto_increment)]
    pub id: i64,
    
    // Default/fallback values
    pub name: String,
    pub description: String,
    
    pub price: f64,
    
    // JSONB column for translations
    pub translations: Option<Json>,
}

// Set individual translation
product.set_translation("name", "ar", "اسم المنتج")?;
product.set_translation("name", "fr", "Nom du produit")?;
product.set_translation("description", "ar", "وصف المنتج")?;

// Set multiple translations at once
let mut names = HashMap::new();
names.insert("en", "Product Name");
names.insert("ar", "اسم المنتج");
names.insert("fr", "Nom du produit");
product.set_translations("name", names)?;

// Get specific translation
if let Some(name) = product.get_translation("name", "ar")? {
    println!("Arabic name: {}", name);
}

// Get with fallback chain: requested -> fallback language -> default field value
let name = product.get_translated("name", "ar")?;

// Get all translations for a field
let all_names = product.get_all_translations("name")?;
for (lang, value) in all_names {
    println!("{}: {}", lang, value);
}

// Get all translations for a language
let arabic = product.get_translations_for_language("ar")?;
// Returns: {"name": "اسم المنتج", "description": "وصف المنتج"}

// Check translations
if product.has_translation("name", "ar")? {
    println!("Arabic name available");
}

let languages = product.available_languages("name")?;
println!("Name available in: {:?}", languages);

// JSON output with translated fields
let mut opts = HashMap::new();
opts.insert("language".to_string(), "ar".to_string());
let json = product.to_translated_json(Some(opts));

// Remove translations
product.remove_translation("name", "fr")?;
product.remove_field_translations("name")?;
product.clear_translations()?;

product.update().await?;

Model Validation

NEW in v0.4: Comprehensive validation system with built-in rules!

use tideorm::validation::{Validator, ValidationRule};

// Available validation rules
ValidationRule::Required           // Field must not be empty
ValidationRule::Email              // Valid email format
ValidationRule::Url                // Valid URL format
ValidationRule::MinLength(n)       // Minimum string length
ValidationRule::MaxLength(n)       // Maximum string length
ValidationRule::Min(n)             // Minimum numeric value
ValidationRule::Max(n)             // Maximum numeric value
ValidationRule::Range(min, max)    // Numeric range
ValidationRule::Regex(pattern)     // Custom regex pattern
ValidationRule::Alpha              // Only alphabetic characters
ValidationRule::Alphanumeric       // Only alphanumeric characters
ValidationRule::Numeric            // Only numeric characters
ValidationRule::Uuid               // Valid UUID format
ValidationRule::In(values)         // Value must be in list
ValidationRule::NotIn(values)      // Value must not be in list

// Create validator
let validator = Validator::new()
    .field("email", vec![ValidationRule::Required, ValidationRule::Email])
    .field("username", vec![
        ValidationRule::Required,
        ValidationRule::MinLength(3),
        ValidationRule::MaxLength(20),
        ValidationRule::Alphanumeric,
    ])
    .field("age", vec![ValidationRule::Range(18.0, 120.0)]);

// Validate data
let mut data = HashMap::new();
data.insert("email".to_string(), "user@example.com".to_string());
data.insert("username".to_string(), "johndoe123".to_string());
data.insert("age".to_string(), "25".to_string());

match validator.validate_map(&data) {
    Ok(_) => println!("Validation passed!"),
    Err(errors) => {
        for (field, message) in errors.errors() {
            println!("{}: {}", field, message);
        }
    }
}

// Custom validation logic
let validator = ValidationBuilder::new()
    .add("username", ValidationRule::Required)
    .add("username", ValidationRule::MinLength(3))
    .custom("username", |value| {
        let reserved = ["admin", "root", "system"];
        if reserved.contains(&value.to_lowercase().as_str()) {
            Err(format!("Username '{}' is reserved", value))
        } else {
            Ok(())
        }
    })
    .build();

Full-Text Search

NEW in v0.4: Cross-database full-text search support (PostgreSQL tsvector, MySQL FULLTEXT, SQLite FTS5)!

// Simple full-text search
let results = Article::search(&["title", "content"], "rust programming")
    .await?;

// Search with ranking (ordered by relevance)
let ranked = Article::search_ranked(&["title", "content"], "rust async")
    .limit(10)
    .get_ranked()
    .await?;

for result in ranked {
    println!("{}: {} (rank: {:.2})", 
        result.record.id, 
        result.record.title, 
        result.rank
    );
}

// Count matching results
let count = Article::search(&["title", "content"], "rust")
    .count()
    .await?;

// Get first matching result
let first = Article::search(&["title"], "rust")
    .first()
    .await?;

// Search modes
use tideorm::fulltext::SearchMode;

// Natural language search (default)
Article::search(&["content"], "learn rust programming").await?;

// Boolean search with operators
Article::search(&["content"], "+rust +async -javascript")
    .mode(SearchMode::Boolean)
    .get()
    .await?;

// Phrase search (exact phrase matching)
Article::search(&["content"], "async await")
    .mode(SearchMode::Phrase)
    .get()
    .await?;

// Prefix search (for autocomplete)
Article::search(&["title"], "prog")
    .mode(SearchMode::Prefix)
    .get()
    .await?;

// Text highlighting
use tideorm::fulltext::{highlight_text, generate_snippet};

let text = "The quick brown fox jumps over the lazy dog.";

// Highlight search terms
let highlighted = highlight_text(text, "fox lazy", "", "");
// Result: "The quick brown fox jumps over the lazy dog."

// Generate snippet with context
let long_text = "Lorem ipsum... The fox jumped... More text here...";
let snippet = generate_snippet(long_text, "fox", 5, "", "");
// Result: "...dolor sit amet. The fox jumped over the..."

Nested Save (Cascade Operations)

NEW in v0.4: Save parent and related models together with automatic foreign key handling!

// Save parent with single related model
let (user, profile) = user.save_with_one(profile, "user_id").await?;
// profile.user_id is automatically set to user.id

// Save parent with multiple related models
let posts = vec![post1, post2, post3];
let (user, posts) = user.save_with_many(posts, "user_id").await?;
// All posts have user_id set to user.id

// Cascade updates
let (user, profile) = user.update_with_one(profile).await?;
let (user, posts) = user.update_with_many(posts).await?;

// Cascade delete (children first for referential integrity)
let deleted_count = user.delete_with_many(posts).await?;

// Builder API for complex nested saves
let (user, related_json) = NestedSaveBuilder::new(user)
    .with_one(profile, "user_id")
    .with_many(posts, "user_id")
    .with_many(comments, "author_id")
    .save()
    .await?;

Join Result Consolidation

NEW in v0.4: Transform flat JOIN results into nested structures!

use tideorm::prelude::JoinResultConsolidator;

// Flat JOIN results: Vec<(Order, LineItem)>
let flat = Order::query()
    .find_also_related::<LineItem>()
    .get()
    .await?;
// [(order1, item1), (order1, item2), (order2, item3)]

// Consolidate into nested: Vec<(Order, Vec<LineItem>)>
let nested = JoinResultConsolidator::consolidate_two(flat, |o| o.id);
// [(order1, [item1, item2]), (order2, [item3])]

// For LEFT JOINs with Option<B>
let nested = JoinResultConsolidator::consolidate_two_optional(flat, |o| o.id);

// Three-level nesting
let flat3: Vec<(Order, LineItem, Product)> = /* ... */;
let nested3 = JoinResultConsolidator::consolidate_three(flat3, |o| o.id, |i| i.id);
// Vec<(Order, Vec<(LineItem, Vec<Product>)>)>

Transactions

// Model-centric transactions
User::transaction(|tx| async move {
    let user = User::create(User { ... }).await?;
    let profile = Profile::create(Profile { user_id: user.id, ... }).await?;
    Ok((user, profile))
}).await?;

// Database-level transactions
db.transaction(|tx| async move {
    // ... operations ...
    Ok(result)
}).await?;

Batch Operations

// Insert multiple records at once
let users = vec![
    User { id: 0, name: "John".into(), email: "john@example.com".into() },
    User { id: 0, name: "Jane".into(), email: "jane@example.com".into() },
];
let inserted = User::insert_all(users).await?;

// Bulk update
let affected = User::update_all()
    .set("active", false)
    .where_eq("last_login_before", "2024-01-01")
    .execute()
    .await?;

// Bulk delete
let deleted = User::query()
    .where_eq("status", "inactive")
    .delete()
    .await?;

Scopes (Reusable Query Fragments)

// Define scope functions
fn active<M: Model>(q: QueryBuilder<M>) -> QueryBuilder<M> {
    q.where_eq("active", true)
}

fn recent<M: Model>(q: QueryBuilder<M>) -> QueryBuilder<M> {
    q.order_desc("created_at").limit(10)
}

// Apply scopes
let users = User::query()
    .scope(active)
    .scope(recent)
    .get()
    .await?;

// Conditional scopes
let include_inactive = false;
let users = User::query()
    .when(include_inactive, |q| q.with_trashed())
    .get()
    .await?;

// Apply scope based on Option value
let status_filter: Option<&str> = Some("active");
let users = User::query()
    .when_some(status_filter, |q, status| q.where_eq("status", status))
    .get()
    .await?;

Callbacks / Hooks

use tideorm::callbacks::Callbacks;

impl Callbacks for User {
    fn before_save(&mut self) -> tideorm::Result<()> {
        // Normalize email before saving
        self.email = self.email.to_lowercase().trim().to_string();
        Ok(())
    }
    
    fn after_create(&self) -> tideorm::Result<()> {
        println!("User {} created with id {}", self.email, self.id);
        // Could send welcome email, create audit log, etc.
        Ok(())
    }
    
    fn before_delete(&self) -> tideorm::Result<()> {
        // Prevent deletion of important accounts
        if self.email == "admin@example.com" {
            return Err(tideorm::Error::validation("Cannot delete admin account"));
        }
        Ok(())
    }
}

Multi-Database Support

The same code works seamlessly across PostgreSQL, MySQL, and SQLite!

// PostgreSQL
TideConfig::init()
    .database("postgres://user:pass@localhost/mydb")
    .connect()
    .await?;

// MySQL / MariaDB
TideConfig::init()
    .database("mysql://user:pass@localhost/mydb")
    .connect()
    .await?;

// SQLite
TideConfig::init()
    .database("sqlite://./data.db?mode=rwc")
    .connect()
    .await?;

// Feature detection
let db_type = Database::global().backend();
if db_type.supports_json() {
    // JSON/JSONB operations available
}
if db_type.supports_window_functions() {
    // OVER(), ROW_NUMBER(), etc.
}
if db_type.supports_cte() {
    // WITH ... AS (Common Table Expressions)
}
if db_type.supports_fulltext_search() {
    // Full-text search capabilities
}

Raw SQL Queries

// Execute raw SQL and return model instances
let users: Vec<User> = Database::raw::<User>(
    "SELECT * FROM users WHERE age > 18"
).await?;

// With parameters (use $1, $2 for PostgreSQL, ? for MySQL/SQLite)
let users: Vec<User> = Database::raw_with_params::<User>(
    "SELECT * FROM users WHERE age > $1 AND status = $2",
    vec![18.into(), "active".into()]
).await?;

// Execute statements (INSERT, UPDATE, DELETE)
let affected = Database::execute(
    "UPDATE users SET active = false WHERE last_login < NOW() - INTERVAL '1 year'"
).await?;

// Execute with parameters
let affected = Database::execute_with_params(
    "DELETE FROM users WHERE status = $1",
    vec!["banned".into()]
).await?;

Query Logging

// Enable in development
TIDE_LOG_QUERIES=true cargo run

// Or programmatically
TideConfig::init()
    .database(url)
    .enable_logging(true)
    .connect()
    .await?;

🔄 Schema Synchronization

⚠️ Warning: Do NOT use sync(true) in production! Use proper migrations instead.

// Development only - auto-sync schema with models
TideConfig::init()
    .database("postgres://localhost/mydb")
    .sync(true)  // Enable auto-sync (development only!)
    .connect()
    .await?;

// Or export schema to a file
TideConfig::init()
    .database("postgres://localhost/mydb")
    .schema_file("schema.sql")  // Generate SQL file
    .connect()
    .await?;

📚 More Resources