# TideORM Documentation
Complete documentation for TideORM - a developer-friendly ORM for Rust.
## Table of Contents
- [Configuration](#configuration)
- [Basic Connection](#basic-connection)
- [Pool Configuration](#pool-configuration)
- [Database Types](#database-types)
- [Data Type Mappings](#data-type-mappings)
- [Rust to SQL Type Reference](#rust-to-sql-type-reference)
- [Date and Time Types](#date-and-time-types)
- [Schema Builder Column Types](#schema-builder-column-types)
- [Model Definition](#model-definition)
- [Model Attributes](#model-attributes)
- [Model Relations](#model-relations)
- [Defining Relations](#defining-relations)
- [Relation Types](#relation-types)
- [Loading Relations](#loading-relations)
- [Many-to-Many Relations](#many-to-many-relations)
- [Polymorphic Relations](#polymorphic-relations)
- [Self-Referencing Relations](#self-referencing-relations)
- [Query Builder](#query-builder)
- [WHERE Conditions](#where-conditions)
- [OR Conditions](#or-conditions)
- [Strongly-Typed Columns](#strongly-typed-columns)
- [Ordering](#ordering)
- [Pagination](#pagination)
- [Execution Methods](#execution-methods)
- [UNION Queries](#union-queries)
- [Window Functions](#window-functions)
- [Common Table Expressions (CTEs)](#common-table-expressions-ctes)
- [Join Result Consolidation](#join-result-consolidation)
- [CRUD Operations](#crud-operations)
- [Nested Save (Cascade Operations)](#nested-save-cascade-operations)
- [Schema Synchronization](#schema-synchronization-development-only)
- [Soft Deletes](#soft-deletes)
- [Scopes](#scopes-reusable-query-fragments)
- [Transactions](#transactions)
- [Auto-Timestamps](#auto-timestamps)
- [Callbacks / Hooks](#callbacks--hooks)
- [Batch Operations](#batch-operations)
- [File Attachments](#file-attachments)
- [File URL Generation](#file-url-generation)
- [Translations (i18n)](#translations-i18n)
- [Model Validation](#model-validation)
- [Record Tokenization](#record-tokenization)
- [Quick Start](#tokenization-quick-start)
- [Available Methods](#tokenization-methods)
- [Custom Encoders](#custom-encoders)
- [Security Notes](#tokenization-security)
- [Full-Text Search](#full-text-search)
- [Multi-Database Support](#multi-database-support)
- [Raw SQL Queries](#raw-sql-queries)
- [Query Logging](#query-logging)
- [Error Handling](#error-handling)
- [SeaORM 2.0 Features](#seaorm-20-features)
- [Examples](#examples)
- [Testing](#testing)
---
## Configuration
### Basic Connection
```rust
// Simple connection
Database::init("postgres://localhost/mydb").await?;
// With TideConfig (recommended)
TideConfig::init()
.database("postgres://localhost/mydb")
.connect()
.await?;
```
### Pool Configuration
```rust
TideConfig::init()
.database("postgres://localhost/mydb")
.max_connections(20) // Maximum pool size
.min_connections(5) // Minimum idle connections
.connect_timeout(Duration::from_secs(10))
.idle_timeout(Duration::from_secs(300))
.max_lifetime(Duration::from_secs(3600))
.connect()
.await?;
```
### Database Types
```rust
TideConfig::init()
.database_type(DatabaseType::Postgres) // or MySQL, SQLite
.database("postgres://localhost/mydb")
.connect()
.await?;
```
---
## Data Type Mappings
This section explains how Rust types map to SQL types across different databases, and how to properly configure your models and migrations.
### Rust to SQL Type Reference
| `i8`, `i16` | SMALLINT | SMALLINT | INTEGER | |
| `i32` | INTEGER | INT | INTEGER | |
| `i64` | BIGINT | BIGINT | INTEGER | Recommended for primary keys |
| `u8`, `u16` | SMALLINT | SMALLINT UNSIGNED | INTEGER | |
| `u32` | INTEGER | INT UNSIGNED | INTEGER | |
| `u64` | BIGINT | BIGINT UNSIGNED | INTEGER | |
| `f32` | REAL | FLOAT | REAL | |
| `f64` | DOUBLE PRECISION | DOUBLE | REAL | |
| `bool` | BOOLEAN | TINYINT(1) | INTEGER | |
| `String` | TEXT | TEXT | TEXT | |
| `Option<T>` | (nullable) | (nullable) | (nullable) | Wraps any type to make it nullable |
| `uuid::Uuid` | UUID | CHAR(36) | TEXT | |
| `rust_decimal::Decimal` | DECIMAL | DECIMAL(65,30) | TEXT | |
| `serde_json::Value` | JSONB | JSON | TEXT | |
| `Vec<u8>` | BYTEA | BLOB | BLOB | Binary data |
| `chrono::NaiveDate` | DATE | DATE | TEXT | Date only |
| `chrono::NaiveTime` | TIME | TIME | TEXT | Time only |
| `chrono::NaiveDateTime` | TIMESTAMP | DATETIME | TEXT | No timezone |
| `chrono::DateTime<Utc>` | **TIMESTAMPTZ** | TIMESTAMP | TEXT | **With timezone** |
### Date and Time Types
TideORM provides proper support for all common date/time scenarios:
#### DateTime with Timezone (Recommended for most cases)
Use `chrono::DateTime<Utc>` for timestamps that should include timezone information:
```rust
use chrono::{DateTime, Utc};
#[tideorm::model]
#[tide(table = "sessions")]
pub struct Session {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub user_id: i64,
pub token: String,
pub expires_at: DateTime<Utc>, // Maps to TIMESTAMPTZ in PostgreSQL
pub created_at: DateTime<Utc>, // Maps to TIMESTAMPTZ in PostgreSQL
pub updated_at: DateTime<Utc>, // Maps to TIMESTAMPTZ in PostgreSQL
}
```
In migrations, use `timestamptz()`:
```rust
schema.create_table("sessions", |t| {
t.id();
t.big_integer("user_id").not_null();
t.string("token").not_null();
t.timestamptz("expires_at").not_null();
t.timestamps(); // Uses TIMESTAMPTZ by default
}).await?;
```
#### DateTime without Timezone
Use `chrono::NaiveDateTime` when you don't need timezone information:
```rust
use chrono::NaiveDateTime;
#[tideorm::model]
#[tide(table = "logs")]
pub struct Log {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub message: String,
pub logged_at: NaiveDateTime, // Maps to TIMESTAMP (no timezone)
}
```
In migrations, use `timestamp()`:
```rust
schema.create_table("logs", |t| {
t.id();
t.text("message").not_null();
t.timestamp("logged_at").default_now();
}).await?;
```
#### Date Only
Use `chrono::NaiveDate` for date-only fields:
```rust
use chrono::NaiveDate;
#[tideorm::model]
#[tide(table = "events")]
pub struct Event {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub name: String,
pub event_date: NaiveDate, // Maps to DATE
}
```
In migrations, use `date()`:
```rust
schema.create_table("events", |t| {
t.id();
t.string("name").not_null();
t.date("event_date").not_null();
}).await?;
```
#### Time Only
Use `chrono::NaiveTime` for time-only fields:
```rust
use chrono::NaiveTime;
#[tideorm::model]
#[tide(table = "schedules")]
pub struct Schedule {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub name: String,
pub start_time: NaiveTime, // Maps to TIME
pub end_time: NaiveTime,
}
```
In migrations, use `time()`:
```rust
schema.create_table("schedules", |t| {
t.id();
t.string("name").not_null();
t.time("start_time").not_null();
t.time("end_time").not_null();
}).await?;
```
### Schema Builder Column Types
The migration schema builder provides convenience methods for all common column types:
#### Numeric Types
```rust
t.small_integer("count"); // SMALLINT
t.integer("quantity"); // INTEGER
t.big_integer("total"); // BIGINT
t.float("rate"); // REAL/FLOAT
t.double("precise_rate"); // DOUBLE PRECISION
t.decimal("price"); // DECIMAL(10,2)
t.decimal_with("amount", 16, 4); // DECIMAL(16,4)
```
#### String Types
```rust
t.string("name"); // VARCHAR(255)
t.text("description"); // TEXT (unlimited)
```
#### Boolean
```rust
t.boolean("active"); // BOOLEAN
```
#### Date/Time Types
```rust
t.date("birth_date"); // DATE
t.time("start_time"); // TIME
t.datetime("logged_at"); // DATETIME (MySQL) / TIMESTAMP (Postgres)
t.timestamp("created_at"); // TIMESTAMP (without timezone)
t.timestamptz("expires_at"); // TIMESTAMPTZ (with timezone) - PostgreSQL
```
#### Special Types
```rust
t.uuid("external_id"); // UUID (Postgres) / CHAR(36) (MySQL)
t.json("metadata"); // JSON
t.jsonb("data"); // JSONB (PostgreSQL only)
t.binary("file_data"); // BYTEA/BLOB
t.integer_array("tag_ids"); // INTEGER[] (PostgreSQL only)
t.text_array("tags"); // TEXT[] (PostgreSQL only)
```
#### Convenience Methods
```rust
t.id(); // BIGSERIAL PRIMARY KEY (auto-increment)
t.big_increments("id"); // Same as id()
t.increments("id"); // INTEGER PRIMARY KEY (auto-increment)
t.foreign_id("user_id"); // BIGINT (for foreign keys)
t.timestamps(); // created_at + updated_at (TIMESTAMPTZ)
t.timestamps_naive(); // created_at + updated_at (TIMESTAMP, no tz)
t.soft_deletes(); // deleted_at (nullable TIMESTAMPTZ)
```
### Complete Migration Example
```rust
use tideorm::prelude::*;
use tideorm::migration::*;
struct CreateUsersTable;
#[async_trait]
impl Migration for CreateUsersTable {
fn version(&self) -> &str { "20260115_001" }
fn name(&self) -> &str { "create_users_table" }
async fn up(&self, schema: &mut Schema) -> Result<()> {
schema.create_table("users", |t| {
t.id(); // id BIGSERIAL PRIMARY KEY
t.string("email").unique().not_null(); // email VARCHAR(255) UNIQUE NOT NULL
t.string("name").not_null(); // name VARCHAR(255) NOT NULL
t.text("bio").nullable(); // bio TEXT NULL
t.boolean("active").default(true); // active BOOLEAN DEFAULT true
t.date("birth_date").nullable(); // birth_date DATE NULL
t.decimal_with("balance", 12, 2) // balance DECIMAL(12,2) DEFAULT 0.00
.default("0.00");
t.jsonb("preferences").nullable(); // preferences JSONB NULL
t.timestamptz("email_verified_at") // email_verified_at TIMESTAMPTZ NULL
.nullable();
t.timestamps(); // created_at, updated_at TIMESTAMPTZ
t.soft_deletes(); // deleted_at TIMESTAMPTZ NULL
}).await?;
// Add custom index
schema.create_index("users", "idx_users_email_active", &["email", "active"], false).await?;
Ok(())
}
async fn down(&self, schema: &mut Schema) -> Result<()> {
schema.drop_table("users").await
}
}
```
### Matching Model Definition
```rust
use chrono::{DateTime, NaiveDate, Utc};
use rust_decimal::Decimal;
#[tideorm::model]
#[tide(table = "users", soft_delete)]
pub struct User {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub email: String,
pub name: String,
pub bio: Option<String>,
pub active: bool,
pub birth_date: Option<NaiveDate>,
pub balance: Decimal,
pub preferences: Option<serde_json::Value>,
pub email_verified_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
}
```
---
## Model Definition
### Default Behavior (Recommended)
The simplest way to define a model - just `#[tideorm::model]`:
```rust
#[tideorm::model]
#[tide(table = "products")]
pub struct Product {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub name: String,
pub price: f64,
}
```
The `#[tideorm::model]` macro automatically implements:
- `Debug` - for printing/logging
- `Clone` - for cloning instances
- `Default` - for creating default instances
- `Serialize` - for JSON serialization
- `Deserialize` - for JSON deserialization
### Custom Implementations (When Needed)
If you need custom implementations, use `skip_derives` and provide your own:
```rust
#[tideorm::model]
#[tide(table = "products", skip_derives)]
#[index("category")]
#[index("active")]
#[index(name = "idx_price_category", columns = "price,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,
}
// Provide your own implementations
impl Debug for Product { /* custom impl */ }
impl Clone for Product { /* custom impl */ }
```
### Model Attributes
#### Struct-Level Attributes
| `#[tide(table = "name")]` | Custom table name |
| `#[tide(skip_derives)]` | Skip auto-generated Debug, Clone, Serialize, Deserialize |
| `#[tide(skip_debug)]` | Skip auto-generated Debug impl only |
| `#[tide(skip_clone)]` | Skip auto-generated Clone impl only |
| `#[tide(skip_serialize)]` | Skip auto-generated Serialize impl only |
| `#[tide(skip_deserialize)]` | Skip auto-generated Deserialize impl only |
| `#[index("col")]` | Create an index |
| `#[unique_index("col")]` | Create a unique index |
| `#[index(name = "idx", columns = "a,b")]` | Named composite index |
#### Field-Level Attributes
| `#[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 |
---
## Model Relations
TideORM supports SeaORM-style relations defined as struct fields. Relations are lazy-loaded on demand.
### Defining Relations
```rust
use tideorm::prelude::*;
#[tideorm::model]
#[tide(table = "users")]
pub struct User {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub name: String,
pub email: 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>,
}
#[tideorm::model]
#[tide(table = "posts")]
pub struct Post {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub user_id: i64,
pub title: String,
pub content: String,
// Inverse: Post belongs to User
#[tide(belongs_to = "User", foreign_key = "user_id")]
pub author: BelongsTo<User>,
// One-to-many: Post has many Comments
#[tide(has_many = "Comment", foreign_key = "post_id")]
pub comments: HasMany<Comment>,
}
```
### Relation Types
| `HasOne<T>` | `has_one` | One-to-one relationship (e.g., User has one Profile) |
| `HasMany<T>` | `has_many` | One-to-many relationship (e.g., User has many Posts) |
| `BelongsTo<T>` | `belongs_to` | Inverse relationship (e.g., Post belongs to User) |
| `HasManyThrough<T, P>` | `has_many_through` | Many-to-many via pivot table |
| `MorphOne<T>` | - | Polymorphic one-to-one |
| `MorphMany<T>` | - | Polymorphic one-to-many |
### Relation Attributes
| `foreign_key` | Foreign key column on related table | Yes |
| `local_key` | Local key (defaults to primary key) | No |
| `owner_key` | Owner key for BelongsTo | No |
| `pivot` | Pivot table name for HasManyThrough | For through relations |
| `related_key` | Related key on pivot table | For through relations |
### Loading Relations
```rust
// Load a HasOne relation
let user = User::find(1).await?.unwrap();
let profile: Option<Profile> = user.profile.load().await?;
// Load a HasMany relation
let posts: Vec<Post> = user.posts.load().await?;
// Load a BelongsTo relation
let post = Post::find(1).await?.unwrap();
let author: Option<User> = post.author.load().await?;
// Check if relation exists
let has_profile = user.profile.exists().await?; // bool
let has_posts = user.posts.exists().await?; // bool
// Count related records
let post_count = user.posts.count().await?; // u64
```
### Loading with Constraints
```rust
// Load posts with custom conditions
.where_eq("published", true)
.where_gt("views", 100)
.order_desc("created_at")
.limit(10)
}).await?;
// Load profile with constraints
let profile = user.profile.load_with(|query| {
query.where_not_null("avatar")
}).await?;
```
### Many-to-Many Relations
```rust
#[tideorm::model]
#[tide(table = "users")]
pub struct User {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub name: String,
// Many-to-many: User has many Roles through user_roles pivot table
#[tide(has_many_through = "Role", pivot = "user_roles", foreign_key = "user_id", related_key = "role_id")]
pub roles: HasManyThrough<Role, UserRole>,
}
#[tideorm::model]
#[tide(table = "roles")]
pub struct Role {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub name: String,
}
#[tideorm::model]
#[tide(table = "user_roles")]
pub struct UserRole {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub user_id: i64,
pub role_id: i64,
}
// Usage
let user = User::find(1).await?.unwrap();
// Load all roles
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 with new set)
user.roles.sync(vec![
serde_json::json!(1),
serde_json::json!(2),
serde_json::json!(3),
]).await?;
```
### Polymorphic Relations
```rust
use tideorm::prelude::*;
// Images can belong to Posts or Videos (polymorphic)
#[tideorm::model]
#[tide(table = "images")]
pub struct Image {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub path: String,
pub imageable_type: String, // "posts" or "videos"
pub imageable_id: i64,
}
#[tideorm::model]
#[tide(table = "posts")]
pub struct Post {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub title: String,
// Polymorphic: Post has many Images
pub images: MorphMany<Image>,
}
// Note: MorphMany/MorphOne require manual setup with .with_parent()
```
---
## Query Builder
TideORM provides a fluent query builder with all common operations.
As of 0.7.2, query execution paths are aligned on parameterized SQL generation for reads, JOIN clauses are validated before execution, and destructive mutations reject incompatible SELECT, JOIN, ORDER BY, GROUP BY, UNION, CTE, and window-function modifiers instead of silently ignoring them.
### WHERE Conditions
**Type-Safe Approach (Recommended)**
Use auto-generated typed columns for compile-time safety. The same `where_eq`, `where_gt`, etc. methods accept both strings and typed columns:
```rust
// Every model gets a `columns` constant with typed column accessors
// User::columns.id, User::columns.name, User::columns.active, etc.
// SAME method works with both strings AND typed columns:
User::query().where_eq("active", true) // String-based (runtime checked)
User::query().where_eq(User::columns.active, true) // Typed column (compile-time checked)
// Type-safe queries with compile-time column name validation
User::query()
.where_eq(User::columns.status, "active")
.where_gt(User::columns.age, 18)
.where_not_null(User::columns.email)
.get()
.await?;
```
**All WHERE Methods Support Both Approaches**
```rust
// Equality - works with string OR typed column
User::query().where_eq("status", "active")
User::query().where_eq(User::columns.status, "active")
User::query().where_not("role", "admin")
User::query().where_not(User::columns.role, "admin")
// Comparison - works with string OR typed column
User::query().where_gt("age", 18)
User::query().where_gt(User::columns.age, 18)
User::query().where_gte("age", 18)
User::query().where_lt("age", 65)
User::query().where_lte("age", 65)
// Pattern matching - works with string OR typed column
User::query().where_like("name", "%John%")
User::query().where_like(User::columns.name, "%John%")
User::query().where_not_like("email", "%spam%")
// IN / NOT IN - works with string OR typed column
User::query().where_in("role", vec!["admin", "moderator"])
User::query().where_in(User::columns.role, vec!["admin", "moderator"])
User::query().where_not_in("status", vec!["banned", "suspended"])
// NULL checks - works with string OR typed column
User::query().where_null("deleted_at")
User::query().where_null(User::columns.deleted_at)
User::query().where_not_null("email_verified_at")
// Range - works with string OR typed column
User::query().where_between("age", 18, 65)
User::query().where_between(User::columns.age, 18, 65)
// Combine conditions (AND)
User::query()
.where_eq(User::columns.active, true)
.where_gt(User::columns.age, 18)
.where_not_null(User::columns.email)
.get()
.await?;
```
> ⚠️ **Tip**: Use typed columns like `User::columns.name` for compile-time safety. Typos in column names will be caught by the compiler instead of at runtime.
### OR Conditions
TideORM provides comprehensive OR support with two approaches: simple OR methods and the fluent OR API. All OR methods support both string column names and typed columns.
#### Simple OR Methods
```rust
// Basic OR conditions (applied at query level)
// Works with both strings and typed columns:
User::query()
.or_where_eq("role", "admin") // String-based
.or_where_eq(User::columns.role, "moderator") // Typed column
.get()
.await?;
// OR with comparison operators
Product::query()
.or_where_gt(Product::columns.price, 1000.0) // price > 1000
.or_where_lt(Product::columns.price, 50.0) // OR price < 50
.get()
.await?; // Gets premium OR budget products
// OR with pattern matching
User::query()
.or_where_like(User::columns.name, "John%") // name LIKE 'John%'
.or_where_like(User::columns.name, "Jane%") // OR name LIKE 'Jane%'
.get()
.await?;
// OR with IN clause
Product::query()
.or_where_in(Product::columns.category, vec!["Electronics", "Books"])
.or_where_eq(Product::columns.featured, true)
.get()
.await?;
// OR with NULL checks
User::query()
.or_where_null(User::columns.deleted_at)
.or_where_gt(User::columns.reactivated_at, some_date)
.get()
.await?;
// OR with BETWEEN
Product::query()
.or_where_between(Product::columns.price, 10.0, 50.0) // Budget range
.or_where_between(Product::columns.price, 500.0, 1000.0) // Premium range
.get()
.await?;
```
#### Fluent OR API (begin_or / end_or)
For complex queries with grouped OR conditions combined with AND, use the fluent OR API:
```rust
// Basic OR group: (category = 'Electronics' OR category = 'Home')
Product::query()
.begin_or()
.or_where_eq(Product::columns.category, "Electronics")
.or_where_eq(Product::columns.category, "Home")
.end_or()
.get()
.await?;
// OR with AND chains: (Apple AND active) OR (Samsung AND featured)
Product::query()
.begin_or()
.or_where_eq("brand", "Apple").and_where_eq("active", true)
.or_where_eq("brand", "Samsung").and_where_eq("featured", true)
.end_or()
.get()
.await?;
// Complex: active AND rating >= 4.0 AND ((Electronics AND price < 1000) OR (Home AND featured))
Product::query()
.where_eq("active", true)
.where_gte("rating", 4.0)
.begin_or()
.or_where_eq("category", "Electronics").and_where_lt("price", 1000.0)
.or_where_eq("category", "Home").and_where_eq("featured", true)
.end_or()
.get()
.await?;
// Multiple sequential OR groups
Product::query()
.where_eq("active", true)
.begin_or()
.or_where_eq("category", "Electronics")
.or_where_eq("category", "Home")
.end_or()
.begin_or()
.or_where_eq("brand", "Apple")
.or_where_eq("brand", "Samsung")
.end_or()
.get()
.await?;
// SQL: WHERE active = true
// AND (category = 'Electronics' OR category = 'Home')
// AND (brand = 'Apple' OR brand = 'Samsung')
```
#### AND Methods within OR Groups
Use `and_where_*` methods to chain AND conditions within an OR branch:
```rust
Product::query()
.begin_or()
// First OR branch: Electronics with price > 500 and good rating
.or_where_eq("category", "Electronics")
.and_where_gt("price", 500.0)
.and_where_gte("rating", 4.5)
// Second OR branch: Home items that are featured
.or_where_eq("category", "Home")
.and_where_eq("featured", true)
// Third OR branch: Any discounted item
.or_where_not_null("discount_percent")
.end_or()
.get()
.await?;
```
Available `and_where_*` methods:
- `and_where_eq(col, val)` - AND column = value
- `and_where_not(col, val)` - AND column != value
- `and_where_gt(col, val)` - AND column > value
- `and_where_gte(col, val)` - AND column >= value
- `and_where_lt(col, val)` - AND column < value
- `and_where_lte(col, val)` - AND column <= value
- `and_where_like(col, pattern)` - AND column LIKE pattern
- `and_where_in(col, values)` - AND column IN (values)
- `and_where_not_in(col, values)` - AND column NOT IN (values)
- `and_where_null(col)` - AND column IS NULL
- `and_where_not_null(col)` - AND column IS NOT NULL
- `and_where_between(col, min, max)` - AND column BETWEEN min AND max
#### Real-World Examples
```rust
// E-commerce: Flash sale eligibility
let flash_sale_products = Product::query()
.where_eq("active", true)
.where_gt("stock", 100)
.where_gte("rating", 4.3)
.where_null("discount_percent") // Not already discounted
.get()
.await?;
// Inventory: Reorder alerts
let reorder_needed = Product::query()
.where_eq("active", true)
.begin_or()
.or_where_lt("stock", 50).and_where_gt("rating", 4.5) // Popular items low
.or_where_lt("stock", 30) // Any item critically low
.end_or()
.order_by("stock", Order::Asc)
.get()
.await?;
// Marketing: Cross-sell recommendations
let recommendations = Product::query()
.where_eq("active", true)
.begin_or()
.or_where_eq("brand", "Apple")
.or_where_eq("brand", "Samsung").and_where_gt("price", 500.0)
.or_where_eq("featured", true).and_where_not("category", "Electronics")
.end_or()
.order_by("rating", Order::Desc)
.limit(10)
.get()
.await?;
// Search: Multi-pattern name matching
let search_results = Product::query()
.begin_or()
.or_where_like("name", "iPhone%")
.or_where_like("name", "Galaxy%")
.or_where_like("name", "%Pro%")
.end_or()
.where_eq("active", true)
.get()
.await?;
// Analytics: Price segmentation
let segmented = Product::query()
.begin_or()
.or_where_eq("category", "Electronics")
.or_where_eq("category", "Books")
.end_or()
.begin_or()
.or_where_gt("price", 1000.0) // Premium
.or_where_lt("price", 50.0) // Budget
.end_or()
.order_by("price", Order::Desc)
.get()
.await?;
```
### Ordering
```rust
// Basic ordering - works with both strings and typed columns
User::query()
.order_by("created_at", Order::Desc) // String-based
.order_by(User::columns.name, Order::Asc) // Typed column
.get()
.await?;
// Convenience methods - also work with typed columns
User::query().order_asc(User::columns.name) // ORDER BY name ASC
User::query().order_desc(User::columns.created_at) // ORDER BY created_at DESC
User::query().latest() // ORDER BY created_at DESC
User::query().oldest() // ORDER BY created_at ASC
```
### Pagination
```rust
// Limit and offset
User::query()
.limit(10)
.offset(20)
.get()
.await?;
// Page-based pagination
User::query()
.page(3, 25) // Page 3, 25 per page
.get()
.await?;
// Aliases
User::query().take(10).skip(20) // Same as limit(10).offset(20)
```
### Execution Methods
```rust
// Get all matching records
let users = User::query()
.where_eq("active", true)
.get()
.await?; // Vec<User>
// Get first record
let user = User::query()
.where_eq("email", "admin@example.com")
.first()
.await?; // Option<User>
// Get first or fail
let user = User::query()
.where_eq("id", 1)
.first_or_fail()
.await?; // Result<User>
// Count (efficient SQL COUNT)
let count = User::query()
.where_eq("active", true)
.count()
.await?; // u64
// Check existence
let exists = User::query()
.where_eq("email", "admin@example.com")
.exists()
.await?; // bool
// Bulk delete (efficient single DELETE statement)
let deleted = User::query()
.where_eq("status", "inactive")
.delete()
.await?; // u64 (rows affected)
```
### UNION Queries
Combine results from multiple queries:
```rust
// UNION - combines results and removes duplicates
let users = User::query()
.where_eq("active", true)
.union(User::query().where_eq("role", "admin"))
.get()
.await?;
// UNION ALL - includes all results (faster, keeps duplicates)
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?;
```
### Window Functions
Perform calculations across sets of rows:
```rust
use tideorm::prelude::*;
// ROW_NUMBER - assign sequential numbers
let products = Product::query()
.row_number("row_num", Some("category"), "price", Order::Desc)
.get_raw()
.await?;
// SQL: ROW_NUMBER() OVER (PARTITION BY "category" ORDER BY "price" DESC) AS "row_num"
// 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 window
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?;
// LEAD - access next row value
let appointments = Appointment::query()
.lead("next_date", "date", 1, None, "patient_id", "date", Order::Asc)
.get_raw()
.await?;
// NTILE - distribute into buckets
let products = Product::query()
.ntile("price_quartile", 4, "price", Order::Asc)
.get_raw()
.await?;
// Custom window function with full control
let results = Order::query()
.window(
WindowFunction::new(WindowFunctionType::Sum("amount".to_string()), "total_sales")
.partition_by("region")
.order_by("month", Order::Asc)
.frame(FrameType::Rows, FrameBound::UnboundedPreceding, FrameBound::CurrentRow)
)
.get_raw()
.await?;
```
### Common Table Expressions (CTEs)
Define temporary named result sets:
```rust
use tideorm::prelude::*;
// 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?;
// CTE with column aliases
let stats = Sale::query()
.with_cte_columns(
"daily_stats",
vec!["sale_date", "total_sales", "order_count"],
"SELECT DATE(created_at), SUM(amount), COUNT(*) FROM sales GROUP BY DATE(created_at)"
)
.where_raw("date IN (SELECT sale_date FROM daily_stats WHERE total_sales > 10000)")
.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?;
```
---
## CRUD Operations
### Create
```rust
let user = User {
id: 0, // Auto-generated
email: "john@example.com".to_string(),
name: "John Doe".to_string(),
active: true,
};
let user = user.save().await?;
println!("Created user with id: {}", user.id);
```
### Read
```rust
// Get all
let users = User::all().await?;
// Find by ID
let user = User::find(1).await?; // Option<User>
// Query builder (see above)
let users = User::query().where_eq("active", true).get().await?;
```
### Update
```rust
let mut user = User::find(1).await?.unwrap();
user.name = "Jane Doe".to_string();
let user = user.update().await?;
```
### Delete
```rust
// Delete instance
let user = User::find(1).await?.unwrap();
user.delete().await?;
// Delete by ID
User::destroy(1).await?;
// Bulk delete
User::query()
.where_eq("active", false)
.delete()
.await?;
```
---
## Schema Synchronization (Development Only)
TideORM can automatically sync your database schema with your models during development:
```rust
TideConfig::init()
.database("postgres://localhost/mydb")
.sync(true) // Enable auto-sync (development only!)
.connect()
.await?;
```
Or export schema to a file:
```rust
TideConfig::init()
.database("postgres://localhost/mydb")
.schema_file("schema.sql") // Generate SQL file
.connect()
.await?;
```
> ⚠️ **Warning**: Do NOT use `sync(true)` in production! Use proper migrations instead.
---
## Soft Deletes
TideORM supports soft deletes for models that have a `deleted_at` column:
```rust
#[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>>,
}
```
### Querying Soft-Deleted Records
```rust
// By default, soft-deleted records are excluded
let active_posts = Post::query().get().await?;
// Include soft-deleted records
let all_posts = Post::query()
.with_trashed()
.get()
.await?;
// Only get soft-deleted records (trash bin)
let trashed_posts = Post::query()
.only_trashed()
.get()
.await?;
```
### Soft Delete Operations
```rust
use tideorm::SoftDelete;
// Soft delete (sets deleted_at to now)
let post = post.soft_delete().await?;
// Restore a soft-deleted record
let post = post.restore().await?;
// Permanently delete
post.force_delete().await?;
```
---
## Scopes (Reusable Query Fragments)
Define reusable query patterns that can be applied to any query:
```rust
// 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
```rust
// Apply scope conditionally
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?;
```
---
## Transactions
TideORM provides clean transaction support:
```rust
// Model-centric transactions
let user = User::create(User { ... }).await?;
let profile = Profile::create(Profile { user_id: user.id, ... }).await?;
// Return Ok to commit, Err to rollback
Ok((user, profile))
}).await?;
// Database-level transactions
Ok(result)
}).await?;
```
If the closure returns `Ok`, the transaction is committed.
If it returns `Err` or panics, the transaction is rolled back.
---
## Auto-Timestamps
automatically manages `created_at` and `updated_at` fields:
```rust
#[tideorm::model]
#[tide(table = "posts")]
pub struct Post {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub title: String,
pub content: 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 {
id: 0,
title: "Hello".into(),
content: "World".into(),
created_at: Utc::now(), // Will be overwritten
updated_at: Utc::now(), // Will be overwritten
};
let post = post.save().await?;
// created_at and updated_at are now set to the current time
post.title = "Updated Title".into();
let post = post.update().await?;
// updated_at is refreshed, created_at remains unchanged
```
---
## Callbacks / Hooks
Implement lifecycle callbacks for your models:
```rust
use tideorm::callbacks::Callbacks;
#[tideorm::model]
#[tide(table = "users")]
pub struct User {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub email: String,
pub password_hash: String,
}
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(())
}
}
```
### Available Callbacks
| `before_validation` | Before validation runs |
| `after_validation` | After validation passes |
| `before_save` | Before create or update |
| `after_save` | After create or update |
| `before_create` | Before inserting new record |
| `after_create` | After inserting new record |
| `before_update` | Before updating existing record |
| `after_update` | After updating existing record |
| `before_delete` | Before deleting record |
| `after_delete` | After deleting record |
---
## Batch Operations
For efficient bulk operations:
```rust
// 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() },
User { id: 0, name: "Bob".into(), email: "bob@example.com".into() },
];
let inserted = User::insert_all(users).await?;
// Bulk update with conditions
let affected = User::update_all()
.set("active", false)
.set("updated_at", Utc::now())
.where_eq("last_login_before", "2024-01-01")
.execute()
.await?;
```
---
## File Attachments
TideORM provides a file attachment system for managing file relationships. Attachments are stored in a JSONB column with metadata.
### Model Setup
```rust
#[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
}
```
### Relation Types
| `has_one_file` | Single file attachment | Avatar, thumbnail, profile picture |
| `has_many_files` | Multiple file attachments | Gallery images, documents, media |
### Attaching Files
```rust
use tideorm::prelude::*;
// Attach a single file (hasOne) - replaces any existing
product.attach("thumbnail", "uploads/thumb.jpg")?;
// Attach multiple files (hasMany) - accumulates
product.attach("images", "uploads/img1.jpg")?;
product.attach("images", "uploads/img2.jpg")?;
// Attach multiple at once
product.attach_many("images", vec![
"uploads/img3.jpg",
"uploads/img4.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)?;
// Save to persist changes
product.update().await?;
```
### Detaching Files
```rust
// Remove thumbnail (hasOne)
product.detach("thumbnail", None)?;
// Remove specific file (hasMany)
product.detach("images", Some("uploads/img1.jpg"))?;
// Remove all files from relation (hasMany)
product.detach("images", None)?;
// Remove multiple specific files
product.detach_many("images", vec!["img2.jpg", "img3.jpg"])?;
product.update().await?;
```
### Syncing Files (Replace All)
```rust
// Replace all images with new ones
product.sync("images", vec![
"uploads/new1.jpg",
"uploads/new2.jpg",
])?;
// Clear all images
product.sync("images", vec![])?;
// Sync with metadata
let attachments = vec![
FileAttachment::with_metadata("img1.jpg", Some("Photo 1"), Some(1024), Some("image/jpeg")),
FileAttachment::with_metadata("img2.jpg", Some("Photo 2"), Some(2048), Some("image/jpeg")),
];
product.sync_with_metadata("images", attachments)?;
product.update().await?;
```
### Getting Files
```rust
// Get single file (hasOne)
if let Some(thumb) = product.get_file("thumbnail")? {
println!("Thumbnail: {}", thumb.key);
println!("Filename: {}", thumb.filename);
println!("Created: {}", thumb.created_at);
if let Some(size) = thumb.size {
println!("Size: {} bytes", size);
}
}
// Get multiple files (hasMany)
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);
}
```
### FileAttachment Structure
Each attachment stores:
| `key` | `String` | File path/key (e.g., "uploads/2024/01/image.jpg") |
| `filename` | `String` | Extracted filename |
| `created_at` | `String` | ISO 8601 timestamp when attached |
| `original_filename` | `Option<String>` | Original filename if different |
| `size` | `Option<u64>` | File size in bytes |
| `mime_type` | `Option<String>` | MIME type |
| `metadata` | `HashMap` | Custom metadata fields |
### JSON Storage Format
Attachments are stored in JSONB with this structure:
```json
{
"thumbnail": {
"key": "uploads/thumb.jpg",
"filename": "thumb.jpg",
"created_at": "2024-01-15T10:30:00Z"
},
"images": [
{
"key": "uploads/img1.jpg",
"filename": "img1.jpg",
"created_at": "2024-01-15T10:30:00Z",
"size": 1048576,
"mime_type": "image/jpeg"
},
{
"key": "uploads/img2.jpg",
"filename": "img2.jpg",
"created_at": "2024-01-15T10:31:00Z"
}
]
}
```
### File URL Generation
TideORM can automatically generate full URLs for file attachments. This is useful when you store file keys/paths in the database but need to serve them from a CDN or storage service.
#### Global Base URL
Configure a base URL that will be prepended to all file keys:
```rust
TideConfig::init()
.database("postgres://localhost/mydb")
.file_base_url("https://cdn.example.com/uploads")
.connect()
.await?;
```
Now when you call `to_json()`, file attachments will include a `url` field:
```json
{
"thumbnail": {
"key": "products/123/thumb.jpg",
"filename": "thumb.jpg",
"url": "https://cdn.example.com/uploads/products/123/thumb.jpg"
}
}
```
#### Custom URL Generator
For more complex URL generation (signed URLs, image transformations, etc.), use a custom generator that receives **both the field name and the full `FileAttachment`**:
```rust
use tideorm::attachments::FileAttachment;
// Define a custom URL generator function with field name and full metadata access
fn smart_url_generator(field_name: &str, file: &FileAttachment) -> String {
// Route based on field name first
match field_name {
"thumbnail" => {
let quality = if file.size.unwrap_or(0) > 500_000 { "60" } else { "auto" };
return format!("https://thumbs.example.com/q_{}/{}", quality, file.key);
}
"avatar" => {
return format!("https://avatars.example.com/w_200,h_200/{}", file.key);
}
_ => {}
}
// Fall back to mime_type routing
match file.mime_type.as_deref() {
Some(m) if m.starts_with("video/") => {
format!("https://stream.example.com/{}", file.key)
}
Some(m) if m.starts_with("image/") => {
let quality = if file.size.unwrap_or(0) > 1_000_000 { "80" } else { "auto" };
format!("https://images.example.com/q_{}/{}", quality, file.key)
}
_ => format!("https://cdn.example.com/{}", file.key),
}
}
// Use it globally
TideConfig::init()
.database("postgres://localhost/mydb")
.file_url_generator(smart_url_generator)
.connect()
.await?;
```
**Parameters available to URL generators:**
- `field_name` - The attachment field name (e.g., "thumbnail", "avatar", "documents")
- `file` - The full `FileAttachment` struct with:
- `key` - Storage key/path
- `filename` - Extracted filename
- `created_at` - Creation timestamp
- `original_filename` - Original upload name (if available)
- `size` - File size in bytes (if available)
- `mime_type` - MIME type (if available)
- `metadata` - Custom HashMap for additional data
#### Model-Specific URL Generator
Override the URL generator for specific models:
```rust
#[tideorm::model]
#[tide(table = "products")]
#[tide(has_one_file = "thumbnail")]
pub struct Product {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub name: String,
pub files: Option<Json>,
}
impl ModelMeta for Product {
// ... other required methods ...
fn file_url_generator() -> FileUrlGenerator {
|field_name, file| {
match field_name {
"thumbnail" => format!("https://products-cdn.example.com/thumb/{}", file.key),
"gallery" => format!("https://products-cdn.example.com/gallery/{}", file.key),
_ => format!("https://products-cdn.example.com/assets/{}", file.key),
}
}
}
}
```
#### Manual URL Generation
Generate URLs programmatically:
```rust
use tideorm::prelude::*;
use tideorm::attachments::FileAttachment;
// Create a FileAttachment for URL generation
let file = FileAttachment::new("uploads/image.jpg");
let url = Config::generate_file_url("thumbnail", &file);
// With metadata for smarter URL generation
let file = FileAttachment::with_metadata(
"uploads/video.mp4",
Some("My Video.mp4"),
Some(50_000_000),
Some("video/mp4"),
);
let url = Config::generate_file_url("video", &file);
// Using model-specific generator
let url = Product::generate_file_url("thumbnail", &file);
// Using FileAttachment method directly
let attachment = product.get_file("thumbnail")?;
if let Some(thumb) = attachment {
let url = thumb.url("thumbnail"); // Uses global generator with field name
// Or with custom generator
let url = thumb.url_with_generator("thumbnail", |field_name, file| {
format!("https://custom-cdn.com/{}/{}", field_name, file.key)
});
}
```
#### URL Generator Priority
URL generators are resolved in this order:
1. **Model-specific generator** - If the model overrides `file_url_generator()`
2. **Global custom generator** - If set via `TideConfig::file_url_generator()`
3. **Global base URL** - If set via `TideConfig::file_base_url()`
4. **Key as-is** - If no configuration, returns the key unchanged
---
## Translations (i18n)
TideORM provides a translation system for multilingual content. Translations are stored in a JSONB column.
### Model Setup
```rust
#[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>,
}
```
### Setting Translations
```rust
use tideorm::prelude::*;
// 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)?;
// Sync translations (replace all for a field)
let mut new_names = HashMap::new();
new_names.insert("en", "New Product Name");
new_names.insert("de", "Neuer Produktname");
product.sync_translations("name", new_names)?;
// Save to persist
product.update().await?;
```
### Getting Translations
```rust
// 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": "وصف المنتج"}
```
### Checking Translations
```rust
// Check if specific translation exists
if product.has_translation("name", "ar")? {
println!("Arabic name available");
}
// Check if field has any translations
if product.has_any_translation("name")? {
println!("Name has translations");
}
// Get available languages for a field
let languages = product.available_languages("name")?;
println!("Name available in: {:?}", languages);
```
### Removing Translations
```rust
// Remove specific translation
product.remove_translation("name", "fr")?;
// Remove all translations for a field
product.remove_field_translations("name")?;
// Clear all translations
product.clear_translations()?;
product.update().await?;
```
### JSON Output with Translations
```rust
// Get JSON with translated fields (removes raw translations column)
let mut opts = HashMap::new();
opts.insert("language".to_string(), "ar".to_string());
let json = product.to_translated_json(Some(opts));
// Result: {"id": 1, "name": "اسم المنتج", "description": "وصف المنتج", "price": 99.99}
// Get JSON with fallback (if Arabic not available, uses fallback language)
let json = product.to_translated_json(Some(opts));
// Get JSON including all translations (for admin interfaces)
let json = product.to_json_with_all_translations();
// Result includes raw translations field
```
### Translation Configuration
When implementing `HasTranslations` manually:
```rust
impl HasTranslations for Product {
fn translatable_fields() -> Vec<&'static str> {
vec!["name", "description"]
}
fn allowed_languages() -> Vec<String> {
vec!["en".to_string(), "ar".to_string(), "fr".to_string(), "de".to_string()]
}
fn fallback_language() -> String {
"en".to_string()
}
fn get_translations_data(&self) -> Result<TranslationsData, TranslationError> {
match &self.translations {
Some(json) => Ok(TranslationsData::from_json(json)),
None => Ok(TranslationsData::new()),
}
}
fn set_translations_data(&mut self, data: TranslationsData) -> Result<(), TranslationError> {
self.translations = Some(data.to_json());
Ok(())
}
fn get_default_value(&self, field: &str) -> Result<serde_json::Value, TranslationError> {
match field {
"name" => Ok(serde_json::json!(self.name)),
"description" => Ok(serde_json::json!(self.description)),
_ => Err(TranslationError::InvalidField(format!("Unknown field: {}", field))),
}
}
}
```
### JSON Storage Format
Translations are stored in JSONB with this structure:
```json
{
"name": {
"en": "Wireless Headphones",
"ar": "سماعات لاسلكية",
"fr": "Écouteurs sans fil"
},
"description": {
"en": "High-quality wireless headphones",
"ar": "سماعات لاسلكية عالية الجودة",
"fr": "Écouteurs sans fil de haute qualité"
}
}
```
### Combining Attachments and Translations
Models can use both features together:
```rust
#[tideorm::model]
#[tide(table = "products")]
#[tide(translatable = "name,description")]
#[tide(has_one_file = "thumbnail")]
#[tide(has_many_files = "images")]
pub struct Product {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub name: String,
pub description: String,
pub price: f64,
pub translations: Option<Json>,
pub files: Option<Json>,
}
// Use both features
product.set_translation("name", "ar", "اسم المنتج")?;
product.attach("thumbnail", "uploads/thumb.jpg")?;
product.attach_many("images", vec!["img1.jpg", "img2.jpg"])?;
product.update().await?;
```
---
## Model Validation
TideORM provides a comprehensive validation system for model data.
### Built-in Validation Rules
```rust
use tideorm::validation::{ValidationRule, Validator, ValidationBuilder};
// 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
```
### Using the Validator
```rust
use tideorm::validation::{Validator, ValidationRule};
use std::collections::HashMap;
// Create a validator with rules
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);
}
}
}
```
### ValidationBuilder with Custom Rules
```rust
use tideorm::validation::ValidationBuilder;
let validator = ValidationBuilder::new()
.add("email", ValidationRule::Required)
.add("email", ValidationRule::Email)
.add("username", ValidationRule::Required)
.add("username", ValidationRule::MinLength(3))
// Add custom validation logic
.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();
```
### Handling Validation Errors
```rust
use tideorm::validation::ValidationErrors;
let mut errors = ValidationErrors::new();
errors.add("email", "Email is required");
errors.add("email", "Email format is invalid");
errors.add("password", "Password must be at least 8 characters");
// Check if there are errors
if errors.has_errors() {
// Get all errors for a specific field
let email_errors = errors.field_errors("email");
for msg in email_errors {
println!("Email error: {}", msg);
}
// Display all errors
println!("{}", errors);
}
// Convert to TideORM Error
let tide_error: tideorm::error::Error = errors.into();
```
---
## Record Tokenization
TideORM provides secure tokenization for record IDs, converting them to encrypted, URL-safe tokens. This prevents exposing internal database IDs in URLs and APIs.
### Tokenization Quick Start
Enable tokenization with the `#[tide(tokenize)]` attribute:
```rust
use tideorm::prelude::*;
#[derive(Model)]
#[tide(table = "users", tokenize)] // Enable tokenization
pub struct User {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub email: String,
pub name: String,
}
// Configure encryption key once at startup
TokenConfig::set_encryption_key("your-32-byte-secret-key-here-xx");
// Tokenize a record
let user = User::find(1).await?.unwrap();
let token = user.tokenize()?; // "iIBmdKYhJh4_vSKFlBTP..."
// Decode token to ID (doesn't hit database)
let id = User::detokenize(&token)?; // 1
// Fetch record directly from token
let same_user = User::from_token(&token).await?;
assert_eq!(user.id, same_user.id);
```
### Tokenization Methods
When a model has `#[tide(tokenize)]`, these methods are available:
| `user.tokenize()` | Convert record to token (instance method) |
| `user.to_token()` | Alias for `tokenize()` |
| `User::tokenize_id(42)` | Tokenize an ID without having the record |
| `User::detokenize(&token)` | Decode token to ID (doesn't fetch from DB) |
| `User::decode_token(&token)` | Alias for `detokenize()` |
| `User::from_token(&token).await` | Decode token and fetch record from DB |
| `user.regenerate_token()` | Generate a new token (same as tokenize) |
### Model-Specific Tokens
Tokens are bound to their model type. A User token cannot decode a Product:
```rust
#[derive(Model)]
#[tide(table = "users", tokenize)]
pub struct User { /* ... */ }
#[derive(Model)]
#[tide(table = "products", tokenize)]
pub struct Product { /* ... */ }
// Same ID, different tokens
let user_token = User::tokenize_id(1)?;
let product_token = Product::tokenize_id(1)?;
assert_ne!(user_token, product_token); // Different!
// Cross-model decoding fails
assert!(User::detokenize(&product_token).is_err()); // Error!
```
### Using Tokens in APIs
Tokens are URL-safe and perfect for REST APIs:
```rust
// In your API handler
async fn get_user(token: String) -> Result<Json<User>> {
let user = User::from_token(&token).await?;
Ok(Json(user))
}
// Example URLs:
// GET /api/users/iIBmdKYhJh4_vSKFlBTPgWRlbW8tZW5isZqLo_EU4YI
// GET /api/products/1NhY5XxAm_D53flvEc-5JmRlbW8tZW5iShKwXZjCb9s
```
### Custom Encoders
For custom tokenization logic, implement the `Tokenizable` trait manually:
```rust
use tideorm::tokenization::{Tokenizable, TokenEncoder, TokenDecoder};
#[derive(Model)]
#[tide(table = "documents")]
pub struct Document {
#[tide(primary_key)]
pub id: i64,
pub title: String,
}
#[async_trait::async_trait]
impl Tokenizable for Document {
fn token_model_name() -> &'static str { "Document" }
fn token_primary_key(&self) -> i64 { self.id }
// Custom encoder - prefix with "DOC-"
fn token_encoder() -> Option<TokenEncoder> {
Some(|id, _model| Ok(format!("DOC-{}", id)))
}
// Custom decoder
fn token_decoder() -> Option<TokenDecoder> {
Some(|token, _model| {
token.strip_prefix("DOC-")?.parse().ok()
})
}
async fn from_token(token: &str) -> tideorm::Result<Self> {
let id = Self::decode_token(token)?;
Self::find(id).await?.ok_or_else(||
tideorm::Error::not_found("Document not found")
)
}
}
```
### Global Custom Encoder
Set a custom encoder for all models:
```rust
// Set global custom encoder
TokenConfig::set_encoder(|id, model| {
Ok(format!("{}-{}", model.to_lowercase(), id))
});
TokenConfig::set_decoder(|token, model| {
let prefix = format!("{}-", model.to_lowercase());
token.strip_prefix(&prefix)?.parse().ok()
});
```
### Tokenization Security
**Features:**
- **XOR encryption** with HMAC integrity verification
- **Model binding**: HMAC includes model name, preventing cross-model reuse
- **Tamper detection**: Modified tokens are rejected
- **URL-safe**: Base64-URL encoding (A-Za-z0-9-_), no escaping needed
**Best Practices:**
- Use a secure 32+ character encryption key in production
- Store keys in environment variables, never in code
- Changing the key invalidates all existing tokens
- Consider token rotation for high-security applications
```rust
// Configure from environment variable
TokenConfig::set_encryption_key(
&std::env::var("ENCRYPTION_KEY").expect("ENCRYPTION_KEY must be set")
);
```
---
## Full-Text Search
provides full-text search capabilities across PostgreSQL (tsvector/tsquery), MySQL (FULLTEXT), and SQLite (FTS5).
### Search Basics
```rust
use tideorm::prelude::*;
// 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
```rust
use tideorm::fulltext::{SearchMode, FullTextConfig};
// 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?;
```
### Search Configuration
```rust
use tideorm::fulltext::{FullTextConfig, SearchMode, SearchWeights};
let config = FullTextConfig::new()
.language("english") // Text analysis language
.mode(SearchMode::Boolean) // Search mode
.min_word_length(3) // Minimum word length to index
.max_word_length(50) // Maximum word length
// Custom weights for ranking (title > summary > content)
.weights(SearchWeights::new(1.0, 0.5, 0.3, 0.1));
let results = Article::search_with_config(
&["title", "summary", "content"],
"rust programming",
config
).get().await?;
```
### Text Highlighting
```rust
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", "<mark>", "</mark>");
// Result: "The quick brown <mark>fox</mark> jumps over the <mark>lazy</mark> dog."
// Generate snippet with context
let long_text = "Lorem ipsum... The fox jumped... More text here...";
let snippet = generate_snippet(long_text, "fox", 5, "<b>", "</b>");
// Result: "...dolor sit amet. The <b>fox</b> jumped over the..."
```
### Creating Full-Text Indexes
```rust
use tideorm::fulltext::{FullTextIndex, PgFullTextIndexType};
use tideorm::config::DatabaseType;
// Create index definition
let index = FullTextIndex::new(
"idx_articles_search",
"articles",
vec!["title".to_string(), "content".to_string()]
)
.language("english")
.pg_index_type(PgFullTextIndexType::GIN);
// Generate SQL for your database
let sql = index.to_sql(DatabaseType::Postgres);
// PostgreSQL: CREATE INDEX "idx_articles_search" ON "articles"
// USING GIN ((to_tsvector('english', ...)))
let sql = index.to_sql(DatabaseType::MySQL);
// MySQL: CREATE FULLTEXT INDEX `idx_articles_search` ON `articles`(`title`, `content`)
let sql = index.to_sql(DatabaseType::SQLite);
// SQLite: Creates FTS5 virtual table + sync triggers
```
### PostgreSQL-Specific Features
```rust
use tideorm::fulltext::pg_headline_sql;
// Generate ts_headline SQL for server-side highlighting
let headline_sql = pg_headline_sql(
"content", // column
"search query", // search terms
"english", // language
"<b>", "</b>" // highlight tags
);
// Result: ts_headline('english', "content", plainto_tsquery(...), ...)
```
---
## Multi-Database Support
automatically detects your database type and generates appropriate SQL syntax. The same code works seamlessly across PostgreSQL, MySQL, and SQLite.
### Connecting to Different Databases
```rust
// 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?;
```
### Explicit Database Type
```rust
TideConfig::init()
.database_type(DatabaseType::MySQL)
.database("mysql://localhost/mydb")
.connect()
.await?;
```
### Database Feature Detection
Check which features are supported by the current database:
```rust
let db_type = require_db()?.backend();
// Feature checks
if db_type.supports_json() {
// JSON/JSONB operations available
}
if db_type.supports_arrays() {
// Native array operations (PostgreSQL only)
}
if db_type.supports_returning() {
// RETURNING clause for INSERT/UPDATE
}
if db_type.supports_upsert() {
// ON CONFLICT / ON DUPLICATE KEY support
}
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
}
```
### Database-Specific JSON Operations
automatically translates JSON queries to the appropriate syntax:
```rust
// This query works on all databases with JSON support
Product::query()
.where_json_contains("metadata", serde_json::json!({"featured": true}))
.get()
.await?;
```
**Generated SQL by database:**
| JSON Contains | `col @> '{"key":1}'` | `JSON_CONTAINS(col, '{"key":1}')` | `json_each(col)` + subquery |
| Key Exists | `col ? 'key'` | `JSON_CONTAINS_PATH(col, 'one', '$.key')` | `json_extract(col, '$.key') IS NOT NULL` |
| Path Exists | `col @? '$.path'` | `JSON_CONTAINS_PATH(col, 'one', '$.path')` | `json_extract(col, '$.path') IS NOT NULL` |
### Database-Specific Array Operations
Array operations are fully supported on PostgreSQL. On MySQL/SQLite, arrays are stored as JSON:
```rust
// PostgreSQL native arrays
Product::query()
.where_array_contains("tags", vec!["sale", "featured"])
.get()
.await?;
```
**Generated SQL:**
| Contains | `col @> ARRAY['a','b']` | `JSON_CONTAINS(col, '["a","b"]')` |
| Contained By | `col <@ ARRAY['a','b']` | `JSON_CONTAINS('["a","b"]', col)` |
| Overlaps | `col && ARRAY['a','b']` | `JSON_OVERLAPS(col, '["a","b"]')` (MySQL 8+) |
### Database-Specific Optimizations
applies optimizations based on your database:
| Optimal Batch Size | 1000 | 1000 | 500 |
| Parameter Style | `$1, $2, ...` | `?, ?, ...` | `?, ?, ...` |
| Identifier Quoting | `"column"` | `` `column` `` | `"column"` |
| Float Casting | `FLOAT8` | `DOUBLE` | `REAL` |
### Feature Compatibility Matrix
| JSON/JSONB | ✅ | ✅ | ✅ (JSON1) |
| Native JSON Operators | ✅ | ✅ | ❌ |
| Native Arrays | ✅ | ❌ | ❌ |
| RETURNING Clause | ✅ | ❌ | ✅ (3.35+) |
| Upsert | ✅ | ✅ | ✅ |
| Window Functions | ✅ | ✅ (8.0+) | ✅ (3.25+) |
| CTEs | ✅ | ✅ (8.0+) | ✅ (3.8+) |
| Full-Text Search | ✅ | ✅ | ✅ (FTS5) |
| Schemas | ✅ | ✅ | ❌ |
---
## Raw SQL Queries
For complex queries that can't be expressed with the query builder:
```rust
// 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 raw SQL statement (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 SQL query logging for development/debugging:
```bash
# Set environment variable
TIDE_LOG_QUERIES=true cargo run
```
When enabled, all SQL queries will be logged to stderr.
---
## Error Handling
provides rich error types with optional context:
```rust
// Get context from errors
if let Err(e) = User::find_or_fail(999).await {
if let Some(ctx) = e.context() {
println!("Error in table: {:?}", ctx.table);
println!("Column: {:?}", ctx.column);
println!("Query: {:?}", ctx.query);
}
}
// Create errors with context
use tideorm::error::{Error, ErrorContext};
let ctx = ErrorContext::new()
.table("users")
.column("email")
.query("SELECT * FROM users WHERE email = $1");
return Err(Error::not_found("User not found").with_context(ctx));
```
---
## SeaORM 2.0 Features
TideORM includes all major features from SeaORM 2.0:
### Strongly-Typed Columns
Compile-time type safety for column operations. The compiler catches type mismatches before runtime.
**Auto-Generated Columns**
When you define a model with `#[tideorm::model]`, typed columns are automatically generated as an attribute on the model:
```rust
#[tideorm::model]
#[tide(table = "users")]
pub struct User {
#[tide(primary_key, auto_increment)]
pub id: i64,
pub name: String,
pub age: Option<i32>,
pub active: bool,
}
// A `UserColumns` struct is automatically generated with typed column accessors.
// Access columns via `User::columns`:
// User::columns.id, User::columns.name, User::columns.age, User::columns.active
```
**Unified Type-Safe Queries**
All query methods accept BOTH string column names AND typed columns. Use `User::columns.field_name` for compile-time safety:
```rust
// SAME method works with both strings AND typed columns:
User::query().where_eq("name", "Alice") // String-based (runtime checked)
User::query().where_eq(User::columns.name, "Alice") // Typed column (compile-time checked)
// Type-safe query - compiler catches typos!
let users = User::query()
.where_eq(User::columns.name, "Alice") // ✓ Type-safe
.where_gt(User::columns.age, 18) // ✓ Type-safe
.where_eq(User::columns.active, true) // ✓ Type-safe
.get()
.await?;
// All query methods support typed columns:
User::query().where_eq(User::columns.name, "Alice") // =
User::query().where_not(User::columns.role, "admin") // <>
User::query().where_gt(User::columns.age, 18) // >
User::query().where_gte(User::columns.age, 18) // >=
User::query().where_lt(User::columns.age, 65) // <
User::query().where_lte(User::columns.age, 65) // <=
User::query().where_like(User::columns.email, "%@test.com") // LIKE
User::query().where_not_like(User::columns.email, "%spam%") // NOT LIKE
User::query().where_in(User::columns.role, vec!["admin", "mod"]) // IN
User::query().where_not_in(User::columns.status, vec!["banned"]) // NOT IN
User::query().where_null(User::columns.deleted_at) // IS NULL
User::query().where_not_null(User::columns.email) // IS NOT NULL
User::query().where_between(User::columns.age, 18, 65) // BETWEEN
// Ordering and grouping also support typed columns:
User::query()
.order_by(User::columns.created_at, Order::Desc)
.order_asc(User::columns.name)
.group_by(User::columns.role)
.get()
.await?;
// Aggregations with typed columns:
let total = Order::query().sum(Order::columns.amount).await?;
let average = Product::query().avg(Product::columns.price).await?;
let max_age = User::query().max(User::columns.age).await?;
// OR conditions with typed columns:
User::query()
.or_where_eq(User::columns.role, "admin")
.or_where_eq(User::columns.role, "moderator")
.get()
.await?;
```
**Why Use Typed Columns?**
- **Compile-time safety**: Wrong column names won't compile
- **IDE autocomplete**: `User::columns.` shows all available columns with their types
- **Refactoring-friendly**: Rename a field and the compiler tells you everywhere to update
- **No conflicts**: Columns are accessed via `.columns`, won't override other struct attributes
- **Backward compatible**: String column names still work for quick prototyping
**Manual Column Definitions (Advanced)**
If you need custom behavior or computed columns, you can define columns manually:
```rust
use tideorm::columns::Column;
// Custom columns that map to different DB column names
pub const FULL_NAME: Column<String> = Column::new("full_name");
pub const COMPUTED_FIELD: Column<i32> = Column::new("computed_field");
// Use in queries
User::query().where_eq(FULL_NAME, "John Doe").get().await?;
```
**Typed Column Support Summary:**
All these methods accept both `"column_name"` (string) and `Model::columns.field` (typed):
| **WHERE** | `where_eq`, `where_not`, `where_gt`, `where_gte`, `where_lt`, `where_lte`, `where_like`, `where_not_like`, `where_in`, `where_not_in`, `where_null`, `where_not_null`, `where_between` |
| **OR WHERE** | `or_where_eq`, `or_where_not`, `or_where_gt`, `or_where_gte`, `or_where_lt`, `or_where_lte`, `or_where_like`, `or_where_in`, `or_where_not_in`, `or_where_null`, `or_where_not_null`, `or_where_between` |
| **ORDER BY** | `order_by`, `order_asc`, `order_desc` |
| **GROUP BY** | `group_by` |
| **Aggregations** | `sum`, `avg`, `min`, `max`, `count_distinct` |
| **HAVING** | `having_sum_gt`, `having_avg_gt` |
| **Window** | `partition_by`, `order_by` (in WindowFunctionBuilder) |
### Self-Referencing Relations
Support for hierarchical data like org charts, categories, or comment threads:
```rust
#[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>,
}
// Usage:
let emp = Employee::find(5).await?;
// Load parent (manager)
let manager = emp.manager.load().await?;
let has_manager = emp.manager.exists().await?;
// Load children (direct reports)
let reports = emp.reports.load().await?;
let count = emp.reports.count().await?;
// Load entire subtree recursively
let tree = emp.reports.load_tree(3).await?; // 3 levels deep
```
### Nested Save (Cascade Operations)
Save parent and related models together with automatic foreign key handling:
```rust
// 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
Transform flat JOIN results into nested structures:
```rust
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>
// 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>)>)>
```
### Linked Partial Select
Select specific columns from related tables with automatic JOINs:
```rust
// Select specific columns from both tables
let results = User::query()
.select_with_linked::<Profile>(
&["id", "name"], // Local columns
&["bio", "avatar_url"], // Linked columns
"user_id" // Foreign key for join
)
.get::<(i64, String, String, Option<String>)>()
.await?;
// All local columns + specific linked columns
let results = User::query()
.select_also_linked::<Profile>(
&["bio"], // Just the linked columns
"user_id"
)
.get::<(User, String)>()
.await?;
```
### Additional SeaORM 2.0 Features
```rust
// has_related() - EXISTS subqueries
let cakes = Cake::query()
.has_related("fruits", "cake_id", "id", "name", "Mango")
.get().await?;
// eq_any() / ne_all() - PostgreSQL array optimizations
let users = User::query()
.eq_any("id", vec![1, 2, 3, 4, 5]) // "id" = ANY(ARRAY[...])
.ne_all("role", vec!["banned"]) // "role" <> ALL(ARRAY[...])
.get().await?;
// Unix timestamps
use tideorm::types::{UnixTimestamp, UnixTimestampMillis};
let ts = UnixTimestamp::now();
let dt = ts.to_datetime();
// Insert many with returning
let users: Vec<User> = User::insert_many_returning(vec![u1, u2]).await?;
// consolidate() - Reusable query fragments
let active_scope = User::query()
.where_eq("status", "active")
.consolidate();
let admins = User::query().apply(&active_scope).where_eq("role", "admin").get().await?;
// Multi-column unique constraints (migrations)
builder.unique(&["user_id", "role_id"]);
builder.unique_named("uq_email_tenant", &["email", "tenant_id"]);
// CHECK constraints (migrations)
builder.string("email").check("email LIKE '%@%'");
```
---
## Examples
See the [examples] repository for complete working examples:
Repository: https://github.com/mohamadzoh/tideorm-examples
---
## Testing
```bash
# Run all tests
cargo test --features postgres
# Run specific test
cargo test query_builder --features postgres
# Run with all features
cargo test --all-features
```
See [tests/TEST_GUIDE.md](tests/TEST_GUIDE.md) for detailed testing information.