ORSO
ORSO is a Rust ORM (Object-Relational Mapping) library for working with SQLite and Turso databases. It provides a straightforward way to define database schemas using Rust structs and perform common database operations.
Features
- Derive-based schema definition: Use
#[derive(Orso)]to automatically generate database schema from Rust structs - Multiple database modes: Support for local SQLite, remote Turso, sync, and embedded modes
- Automatic schema management: Generate SQL schema and handle migrations
- CRUD operations: insert, read, update, and delete records
- Batch operations: Efficient handling of multiple records
- Query building: Flexible query construction with filtering and sorting
- Pagination: Support for paginated results
- Foreign key relationships: Define relationships between tables
- Type mapping: Automatic conversion between Rust types and database types
- Utility operations: Existence checks, field-based queries, latest/first record finding, and batch ID operations
- Runtime table selection: Use
_with_tablemethods to work with multiple tables using the same struct
Installation
Add ORSO to your Cargo.toml:
[]
= "version"
= { = "1.0", = ["full"] }
Quick Start
1. Define Your Model
use ;
use ;
2. Initialize Database Connection
use ;
// Local SQLite database
let config = local;
let db = init.await?;
3. Run Migrations
use ;
// Create tables automatically with default config
init.await?;
// Or with custom migration config
use MigrationConfig;
let config = MigrationConfig ;
init_with_config.await?;
Custom Table Names: Override the default table name when you need multiple tables with the same schema:
// Use default table name from struct
migration! // Creates "users" table
// Override with custom table name
migration! // Creates "users_archive" table
migration! // Creates "users_backup" table
Migration Safety & Backup Management: ORSO automatically manages migration backups with zero data loss:
- Zero-loss migrations: Original data is always backed up before schema changes
- Smart cleanup: Automatically removes old migration tables based on count and age
- Configurable retention: Control how many backups to keep and for how long
- Clear naming: Migration tables use
_migration_suffix for clarity (e.g.,users_migration_1234567890)
4. Perform CRUD Operations
// Create
let user = User ;
user.insert.await?;
// Read
let user = find_by_id.await?;
let all_users = find_all.await?;
let count = count.await?;
// Update
if let Some = find_by_id.await?
// Delete
if let Some = find_by_id.await?
5. Use New Utility Methods
use ;
// Existence checks (very efficient - returns bool without fetching data)
let has_users = exists.await?;
let has_adults = exists_filter.await?;
// Find by any field
let johns = find_by_field.await?;
let gmail_users = find_by_field.await?;
// Find latest/first records with filters
let filter = filter_op!;
let latest_adult = find_latest_filter.await?;
let first_adult = find_first_filter.await?;
// Find latest/first by specific field
let latest_john = find_latest_by_field.await?;
// Batch operations for performance
let user_ids = vec!;
let users = find_by_ids.await?;
let ages = vec!;
let specific_ages = find_by_field_in.await?;
println!;
Custom Table Operations (_with_table methods)
All CRUD operations have _with_table variants that allow you to specify a custom table name at runtime, enabling one struct to work with multiple tables:
// Create in custom table
user.insert_with_table.await?;
// Read from custom table
let user = find_by_id_with_table.await?;
let all_users = find_all_with_table.await?;
// Update in custom table
user.update_with_table.await?;
// Delete from custom table
user.delete_with_table.await?;
// Count in custom table
let count = count_with_table.await?;
Complete Example: Multiple Tables from One Struct
Here's a practical example of using one struct to insert and manage multiple tables:
use ;
use ;
async
Available _with_table Methods
All standard operations have _with_table variants:
CRUD Operations:
insert_with_table(&self, db, table_name)find_by_id_with_table(id, db, table_name)find_all_with_table(db, table_name)find_where_with_table(filter, db, table_name)update_with_table(&self, db, table_name)delete_with_table(&self, db, table_name)
Advanced Operations:
insert_or_update_with_table(&self, db, table_name)upsert_with_table(&self, db, table_name)count_with_table(db, table_name)count_where_with_table(filter, db, table_name)
Batch Operations:
batch_insert_with_table(models, db, table_name)batch_update_with_table(models, db, table_name)batch_delete_with_table(ids, db, table_name)batch_upsert_with_table(models, db, table_name)
Query Operations:
find_one_with_table(filter, db, table_name)find_latest_with_table(db, table_name)find_paginated_with_table(pagination, db, table_name)find_where_paginated_with_table(filter, pagination, db, table_name)search_with_table(search_filter, pagination, db, table_name)list_with_table(sort, pagination, db, table_name)list_where_with_table(filter, sort, pagination, db, table_name)delete_where_with_table(filter, db, table_name)aggregate_with_table(function, column, filter, db, table_name)
Utility Operations (New!):
exists_with_table(db, table_name)- Check if any records existexists_filter_with_table(filter, db, table_name)- Check if filtered records existfind_latest_filter_with_table(filter, db, table_name)- Find latest record matching filterfind_first_filter_with_table(filter, db, table_name)- Find oldest record matching filterfind_by_field_with_table(field, value, db, table_name)- Find records by any fieldfind_latest_by_field_with_table(field, value, db, table_name)- Find latest record by fieldfind_first_by_field_with_table(field, value, db, table_name)- Find oldest record by fieldfind_by_ids_with_table(ids, db, table_name)- Batch find by multiple IDsfind_by_field_in_with_table(field, values, db, table_name)- Find by multiple field values
Utility Operations in Action
These new utility methods make common database patterns much simpler:
Before vs After
Finding Latest Record by Field (Your Use Case):
// Before: Manual filter construction
let filter = filter_op!;
let record = find_latest_filter_with_table.await?;
// After: Direct field query
let record = find_latest_by_field_with_table.await?;
Checking if Data Exists:
// Before: Fetch and check length
let users = find_all.await?;
let exists = !users.is_empty;
// After: Efficient existence check
let exists = exists.await?;
Batch Finding by IDs:
// Before: Multiple individual queries
let mut users = Vecnew;
for id in
// After: Single batch query
let users = find_by_ids.await?;
Real-World Use Cases
Financial Data Processing:
// Check if we have today's data
let today_filter = filter_op!;
let has_todays_data = exists_filter.await?;
// Get latest price for each symbol
let symbols = vec!;
for symbol in symbols
User Management:
// Find all users from specific domains
let domains = vec!;
let users = find_by_field_in.await?;
// Check if any admin users exist
let admin_filter = filter_op!;
let has_admins = exists_filter.await?;
Database Connection Modes
ORSO supports different database connection modes:
use ;
// Local SQLite file
let local_config = local;
// Remote Turso database
let remote_config = remote;
// Local database with sync to Turso
let sync_config = sync;
// Embedded replica with remote sync
let embed_config = embed;
let db = init.await?;
Schema Definition
Define your database schema using Rust structs with the Orso derive macro:
Basic Fields
Column Attributes
Migrations
ORSO provides automatic zero-loss migrations with smart backup management:
Basic Migration Setup
use ;
// Initialize multiple tables with default settings
init.await?;
Advanced Migration Configuration
use ;
// Custom migration configuration
let config = MigrationConfig ;
// Apply custom config to migrations
init_with_config.await?;
Migration Configuration Options
| Option | Default | Description |
|---|---|---|
max_backups_per_table |
5 |
Maximum number of migration backup tables to keep per original table |
backup_retention_days |
30 |
Delete migration tables older than this many days |
backup_suffix |
"migration" |
Suffix used in migration table names (e.g., table_migration_123456) |
Zero-Loss Migration Process
When ORSO detects schema changes, it automatically:
- Analyzes the current vs expected schema
- Creates a temporary table with the new schema
- Migrates all data from the original table (preserving row order)
- Renames the original table to
{table}_migration_{timestamp} - Renames the temporary table to the original name
- Cleans up old migration tables based on your retention policy
- Verifies migration success
Migration Table Examples
-- Original table
users
-- After migration (backup created)
users -- New schema
users_migration_1234567890 -- Backup with original data
-- After multiple migrations (with cleanup)
users -- Current table
users_migration_1234567890 -- Recent backup
users_migration_1234567891 -- Most recent backup
-- older backups automatically cleaned up
Querying Data
Basic Queries
// Find all records
let users = find_all.await?;
// Find by ID
let user = find_by_id.await?;
// Find with filters
use ;
let filter = filter_op!;
let users = find_where.await?;
Complex Filtering
use ;
// AND conditions
let and_filter = filter_op!;
// OR conditions
let or_filter = filter_op!;
// NOT conditions
let not_filter = filter_op!;
let users = find_where.await?;
Query Builder
use ;
// Basic query with sorting and limits
let results = query!
.select
._where
.order_by
.limit
.
.await?;
// Aggregation queries
let count = query!
.select_count
.execute_count
.await?;
// Group by queries
let results = query!
.select
.group_by
.
.await?;
Batch Operations
For better performance with multiple records:
// Batch insert
let users = vec!;
batch_create.await?;
// Batch update
batch_update.await?;
// Batch delete
let ids = vec!;
batch_delete.await?;
Pagination
ORSO provides built-in pagination support:
use ;
// Offset-based pagination
let pagination = pagination!; // Page 1, 20 items per page
let results = find_paginated.await?;
// Paginated queries with filtering
let filter = filter!;
let results = find_where_paginated.await?;
// Using query builder with pagination
let results = query!
.order_by
.
.await?;
Convenience Macros
ORSO provides several convenience macros for common operations:
use ;
// Filter creation
let eq_filter = filter!;
let gt_filter = filter!;
let in_filter = filter!;
let between_filter = filter!;
let null_filter = filter!;
let not_null_filter = filter!;
// Sorting
let sort_asc = sort!;
let sort_desc = sort!;
let sort_default = sort!; // defaults to ascending
// Pagination
let pagination = pagination!; // page 1, 20 items per page
let default_pagination = pagination!; // page 1, 20 items per page (default)
// Query building
let query_builder = query!;
// Filter operations (combining filters)
let and_filter = filter_op!;
let or_filter = filter_op!;
let not_filter = filter_op!;
let single_filter = filter_op!; // single filter
// Search filters
let search_filter = search!;
Supported Operators
ORSO provides various operators for filtering:
use Operator;
// Equality operators
Eq // Equal (=)
Ne // Not equal (!=)
Lt // Less than (<)
Le // Less than or equal (<=)
Gt // Greater than (>)
Ge // Greater than or equal (>=)
// Pattern matching
Like // LIKE
NotLike // NOT LIKE
// Set operators
In // IN
NotIn // NOT IN
// Null checks
IsNull // IS NULL
IsNotNull // IS NOT NULL
// Range operators
Between // BETWEEN
NotBetween // NOT BETWEEN
Error Handling
ORSO provides comprehensive error handling:
use ;
match find_by_id.await
Supported Data Types
ORSO maps Rust types to SQLite types:
| Rust Type | SQLite Type |
|---|---|
String |
TEXT |
i8, i16, i32, i64 |
INTEGER |
u8, u16, u32, u64 |
INTEGER |
f32, f64 |
REAL |
bool |
INTEGER (0/1) |
Option<T> |
Depends on T (nullable) |
Vec<u8> |
BLOB |
chrono::DateTime<Utc> |
TEXT |
Generated Schema
ORSO automatically generates SQL schema:
-- For a User struct with automatic fields
(
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
name TEXT NOT NULL,
email TEXT NOT NULL,
age INTEGER NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
)
Dependencies
ORSO depends on several key crates:
libsql- The SQLite/Turso database driverserde- Serialization frameworkchrono- Date and time handlinguuid- UUID generationtokio- Async runtimeanyhow- Error handling
Limitations
- Schema changes require running migrations
- Complex relationships may need manual implementation
- Advanced SQL features may require raw queries
- Foreign key values should be retrieved from database operations