TideORM
A developer-friendly ORM for Rust, inspired by Laravel Eloquent, Rails ActiveRecord, and Sequelize.
Features
- Clean Model Definitions - Use
#[derive(Model)]to define your models - Unified Configuration - Database, pool settings, and app config in one place
- Global Database Connection - Initialize once, use anywhere (Rails-style)
- Production-Ready Pool Settings - Configure min/max connections, timeouts
- Fluent Query Builder - Chain methods for readable queries
- Efficient Queries - Optimized COUNT and bulk DELETE operations
- Type Safe - Full Rust type safety without verbose syntax
- Async First - Built for async/await from the ground up
- Database Agnostic - PostgreSQL, MySQL, SQLite support
- Zero SeaORM Exposure - SeaORM is an internal implementation detail
Quick Start
use *;
async
Installation
Add to your Cargo.toml:
[]
= "0.2"
Feature Flags
postgres- PostgreSQL support (default)mysql- MySQL supportsqlite- SQLite supportruntime-tokio- Tokio runtime (default)runtime-async-std- async-std runtime
Configuration
Basic Connection
// Simple connection
init.await?;
// With TideConfig (recommended)
init
.database
.connect
.await?;
Pool Configuration
init
.database
.max_connections // Maximum pool size
.min_connections // Minimum idle connections
.connect_timeout
.idle_timeout
.max_lifetime
.connect
.await?;
Database Types
init
.database_type // or MySQL, SQLite
.database
.connect
.await?;
Model Definition
Model Attributes
| Attribute | Description |
|---|---|
#[tide(table = "name")] |
Custom table name |
#[tide(primary_key)] |
Mark as primary key |
#[tide(auto_increment)] |
Auto-increment field |
#[tide(nullable)] |
Optional/nullable field |
#[index("col")] |
Create an index |
#[unique_index("col")] |
Create a unique index |
#[index(name = "idx", columns = "a,b")] |
Named composite index |
Query Builder
TideORM provides a fluent query builder with all common operations.
WHERE Conditions
// Equality
query.where_eq
query.where_not
// Comparison
query.where_gt // >
query.where_gte // >=
query.where_lt // <
query.where_lte // <=
// Pattern matching
query.where_like
query.where_not_like
// IN / NOT IN
query.where_in
query.where_not_in
// NULL checks
query.where_null
query.where_not_null
// Range
query.where_between
// Combine conditions (AND)
query
.where_eq
.where_gt
.where_not_null
.get
.await?;
Ordering
// Basic ordering
query
.order_by
.order_by
.get
.await?;
// Convenience methods
query.order_asc // ORDER BY name ASC
query.order_desc // ORDER BY created_at DESC
query.latest // ORDER BY created_at DESC
query.oldest // ORDER BY created_at ASC
Pagination
// Limit and offset
query
.limit
.offset
.get
.await?;
// Page-based pagination
query
.page // Page 3, 25 per page
.get
.await?;
// Aliases
query.take.skip // Same as limit(10).offset(20)
Execution Methods
// Get all matching records
let users = query
.where_eq
.get
.await?; // Vec<User>
// Get first record
let user = query
.where_eq
.first
.await?; // Option<User>
// Get first or fail
let user = query
.where_eq
.first_or_fail
.await?; // Result<User>
// Count (efficient SQL COUNT)
let count = query
.where_eq
.count
.await?; // u64
// Check existence
let exists = query
.where_eq
.exists
.await?; // bool
// Bulk delete (efficient single DELETE statement)
let deleted = query
.where_eq
.delete
.await?; // u64 (rows affected)
UNION Queries
Combine results from multiple queries:
// UNION - combines results and removes duplicates
let users = query
.where_eq
.union
.get
.await?;
// UNION ALL - includes all results (faster, keeps duplicates)
let orders = query
.where_eq
.union_all
.union_all
.order_by
.get
.await?;
// Raw UNION for complex queries
let results = query
.union_raw
.get
.await?;
Window Functions
Perform calculations across sets of rows:
use *;
// ROW_NUMBER - assign sequential numbers
let products = query
.row_number
.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 = query
.rank
.get_raw
.await?;
// DENSE_RANK - rank without gaps
let students = query
.dense_rank
.get_raw
.await?;
// Running totals with SUM window
let sales = query
.running_sum
.get_raw
.await?;
// LAG - access previous row value
let orders = query
.lag
.get_raw
.await?;
// LEAD - access next row value
let appointments = query
.lead
.get_raw
.await?;
// NTILE - distribute into buckets
let products = query
.ntile
.get_raw
.await?;
// Custom window function with full control
let results = query
.window
.get_raw
.await?;
Common Table Expressions (CTEs)
Define temporary named result sets:
use *;
// Simple CTE
let orders = query
.with_cte
.where_raw
.get
.await?;
// CTE from another query builder
let active_users = query
.where_eq
.select;
let posts = query
.with_query
.inner_join
.get
.await?;
// CTE with column aliases
let stats = query
.with_cte_columns
.where_raw
.get
.await?;
// Recursive CTE for hierarchical data
let employees = query
.with_recursive_cte
.where_raw
.get
.await?;
CRUD Operations
Create
let user = User ;
let user = user.save.await?;
println!;
Read
// Get all
let users = all.await?;
// Find by ID
let user = find.await?; // Option<User>
// Query builder (see above)
let users = query.where_eq.get.await?;
Update
let mut user = find.await?.unwrap;
user.name = "Jane Doe".to_string;
let user = user.update.await?;
Delete
// Delete instance
let user = find.await?.unwrap;
user.delete.await?;
// Delete by ID
destroy.await?;
// Bulk delete
query
.where_eq
.delete
.await?;
Schema Synchronization (Development Only)
TideORM can automatically sync your database schema with your models during development:
init
.database
.sync // Enable auto-sync (development only!)
.connect
.await?;
Or export schema to a file:
init
.database
.schema_file // 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:
Querying Soft-Deleted Records
// By default, soft-deleted records are excluded
let active_posts = query.get.await?;
// Include soft-deleted records
let all_posts = query
.with_trashed
.get
.await?;
// Only get soft-deleted records (trash bin)
let trashed_posts = query
.only_trashed
.get
.await?;
Soft Delete Operations
use 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:
// Define scope functions
// Apply scopes
let users = query
.scope
.scope
.get
.await?;
Conditional Scopes
// Apply scope conditionally
let include_inactive = false;
let users = query
.when
.get
.await?;
// Apply scope based on Option value
let status_filter: = Some;
let users = query
.when_some
.get
.await?;
Transactions
TideORM provides clean transaction support:
// Model-centric transactions
transaction.await?;
// Database-level transactions
db.transaction.await?;
If the closure returns Ok, the transaction is committed.
If it returns Err or panics, the transaction is rolled back.
Auto-Timestamps
TideORM automatically manages created_at and updated_at fields:
// No need to set timestamps manually
let post = Post ;
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:
use Callbacks;
Available Callbacks
| Callback | When Called |
|---|---|
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:
// Insert multiple records at once
let users = vec!;
let inserted = insert_all.await?;
// Bulk update with conditions
let affected = update_all
.set
.set
.where_eq
.execute
.await?;
File Attachments
TideORM provides a file attachment system inspired by Laravel's HasAttachments and Rails Active Storage. Attachments are stored in a JSONB column with metadata.
Model Setup
Relation Types
| Type | Description | Use Case |
|---|---|---|
has_one_file |
Single file attachment | Avatar, thumbnail, profile picture |
has_many_files |
Multiple file attachments | Gallery images, documents, media |
Attaching Files
use *;
// Attach a single file (hasOne) - replaces any existing
product.attach?;
// Attach multiple files (hasMany) - accumulates
product.attach?;
product.attach?;
// Attach multiple at once
product.attach_many?;
// Attach with metadata
let attachment = with_metadata;
product.attach_with_metadata?;
// Add custom metadata
let attachment = new
.add_metadata
.add_metadata
.add_metadata;
product.attach_with_metadata?;
// Save to persist changes
product.update.await?;
Detaching Files
// Remove thumbnail (hasOne)
product.detach?;
// Remove specific file (hasMany)
product.detach?;
// Remove all files from relation (hasMany)
product.detach?;
// Remove multiple specific files
product.detach_many?;
product.update.await?;
Syncing Files (Replace All)
// Replace all images with new ones
product.sync?;
// Clear all images
product.sync?;
// Sync with metadata
let attachments = vec!;
product.sync_with_metadata?;
product.update.await?;
Getting Files
// Get single file (hasOne)
if let Some = product.get_file?
// Get multiple files (hasMany)
let images = product.get_files?;
for img in images
// Check if has files
if product.has_files?
FileAttachment Structure
Each attachment stores:
| Field | Type | Description |
|---|---|---|
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:
Translations (i18n)
TideORM provides a translation system for multilingual content, inspired by Laravel Translatable and Rails Globalize. Translations are stored in a JSONB column.
Model Setup
Setting Translations
use *;
// Set individual translation
product.set_translation?;
product.set_translation?;
product.set_translation?;
// Set multiple translations at once
let mut names = new;
names.insert;
names.insert;
names.insert;
product.set_translations?;
// Sync translations (replace all for a field)
let mut new_names = new;
new_names.insert;
new_names.insert;
product.sync_translations?;
// Save to persist
product.update.await?;
Getting Translations
// Get specific translation
if let Some = product.get_translation?
// Get with fallback chain: requested -> fallback language -> default field value
let name = product.get_translated?;
// Get all translations for a field
let all_names = product.get_all_translations?;
for in all_names
// Get all translations for a language
let arabic = product.get_translations_for_language?;
// Returns: {"name": "اسم المنتج", "description": "وصف المنتج"}
Checking Translations
// Check if specific translation exists
if product.has_translation?
// Check if field has any translations
if product.has_any_translation?
// Get available languages for a field
let languages = product.available_languages?;
println!;
Removing Translations
// Remove specific translation
product.remove_translation?;
// Remove all translations for a field
product.remove_field_translations?;
// Clear all translations
product.clear_translations?;
product.update.await?;
JSON Output with Translations
// Get JSON with translated fields (removes raw translations column)
let mut opts = new;
opts.insert;
let json = product.to_translated_json;
// 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;
// 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:
JSON Storage Format
Translations are stored in JSONB with this structure:
Combining Attachments and Translations
Models can use both features together:
// Use both features
product.set_translation?;
product.attach?;
product.attach_many?;
product.update.await?;
Multi-Database Support
TideORM automatically detects your database type and generates appropriate SQL syntax. The same code works seamlessly across PostgreSQL, MySQL, and SQLite.
Connecting to Different Databases
// PostgreSQL
init
.database
.connect
.await?;
// MySQL / MariaDB
init
.database
.connect
.await?;
// SQLite
init
.database
.connect
.await?;
Explicit Database Type
init
.database_type
.database
.connect
.await?;
Database Feature Detection
Check which features are supported by the current database:
let db_type = global.backend;
// Feature checks
if db_type.supports_json
if db_type.supports_arrays
if db_type.supports_returning
if db_type.supports_upsert
if db_type.supports_window_functions
if db_type.supports_cte
if db_type.supports_fulltext_search
Database-Specific JSON Operations
TideORM automatically translates JSON queries to the appropriate syntax:
// This query works on all databases with JSON support
query
.where_json_contains
.get
.await?;
Generated SQL by database:
| Operation | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
| 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:
// PostgreSQL native arrays
query
.where_array_contains
.get
.await?;
Generated SQL:
| Operation | PostgreSQL | MySQL/SQLite |
|---|---|---|
| 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
TideORM applies optimizations based on your database:
| Feature | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
| Optimal Batch Size | 1000 | 1000 | 500 |
| Parameter Style | $1, $2, ... |
?, ?, ... |
?, ?, ... |
| Identifier Quoting | "column" |
`column` |
"column" |
| Float Casting | FLOAT8 |
DOUBLE |
REAL |
Feature Compatibility Matrix
| Feature | PostgreSQL | MySQL | SQLite |
|---|---|---|---|
| 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:
// Execute raw SQL and return model instances
let users: = .await?;
// With parameters (use $1, $2 for PostgreSQL, ? for MySQL/SQLite)
let users: = .await?;
// Execute raw SQL statement (INSERT, UPDATE, DELETE)
let affected = execute.await?;
// Execute with parameters
let affected = execute_with_params.await?;
Query Logging
Enable SQL query logging for development/debugging:
# Set environment variable
TIDE_LOG_QUERIES=true
When enabled, all SQL queries will be logged to stderr.
Error Handling
TideORM provides rich error types with optional context:
// Get context from errors
if let Err = find_or_fail.await
// Create errors with context
use ;
let ctx = new
.table
.column
.query;
return Err;
Examples
TideORM provides comprehensive examples organized by category. See examples/README.md for detailed documentation.
Quick Start Examples
CRUD Operations:
basic.rs- Core CRUD operationsupsert_demo.rs- Upsert/on-conflict operations
Query Building:
query_builder.rs- All query builder features
PostgreSQL Features:
postgres_demo.rs- PostgreSQL-specific featurespostgres_complete.rs- Complete feature showcase
Running Examples
# CRUD basics
# Query building
# Upsert operations
# PostgreSQL features
# Complete reference
See examples/README.md for:
- Detailed feature descriptions
- Examples by category
- Learning path recommendations
- Troubleshooting guide
Testing & Development
Test Configuration
TideORM tests, benchmarks, and examples use a .env file for database configuration:
-
Copy the example configuration:
-
Edit
.envwith your database settings:POSTGRESQL_DATABASE_URL=postgres://username:password@host:port/database
See TEST_CONFIG.md for detailed setup instructions.
Running Tests
# Run all tests
# Run specific test suite
# Run benchmarks
Integration Suites
cargo test --test postgres_integration_tests— end-to-end CRUD, scopes, soft deletes, transactions, raw SQL, and batch operations.cargo test --test postgres_advanced_tests— JSON/JSONB operators, array operators, joins, and relation helpers (load_belongs_to,load_has_one,load_has_many).- Both suites expect a reachable PostgreSQL database configured via
TEST_DATABASE_URL(preferred) orPOSTGRESQL_DATABASE_URLin.env(defaults described in TEST_CONFIG.md).
Design Philosophy
TideORM is built with these principles:
- Convention over Configuration - Smart defaults, minimal boilerplate
- Developer Happiness - APIs that feel natural and are hard to misuse
- Abstraction without Leakage - SeaORM internals never leak to users
- Type Safety - Catch errors at compile time when possible
- Performance - Efficient queries (COUNT, bulk DELETE) by default
License
MIT License - see LICENSE for details.
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.