26 KiB
Database Schema
Overview
SQLite database with SeaORM ORM. Uses 8-decimal precision for all monetary values (stored as TEXT). All tables include sync-compatible fields (UUIDs, versions, timestamps).
SeaORM Entities
All database tables are defined as SeaORM entities with derive macros:
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "accounts")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub name: String,
pub account_type: String,
pub currency: String,
pub initial_balance: String,
pub current_balance: String,
pub color: Option<String>,
pub icon: Option<String>,
pub sort_order: i32,
pub is_active: bool,
pub is_archived: bool,
pub include_in_net_worth: bool,
pub show_in_combined_view: bool,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
pub version: i32,
pub device_id: Option<String>,
pub is_deleted: bool,
}
Data Types
- TEXT: Strings, UUIDs, dates (ISO 8601), decimals (stored as strings for precision)
- INTEGER: Booleans (0/1), counts, foreign keys
- JSON: Arrays and objects (SQLite 3.38+)
Precision Handling
All monetary amounts stored as TEXT with 8 decimal places:
1234.56000000
0.00010000
9999999999.99999999
Complete Schema
1. accounts
Multi-account support for tracking different financial accounts.
CREATE TABLE accounts (
id TEXT PRIMARY KEY, -- UUID v4
name TEXT NOT NULL, -- Display name
account_type TEXT NOT NULL CHECK(account_type IN (
'checking', -- Bank checking account
'savings', -- Bank savings account
'credit_card', -- Credit card (negative = owed)
'cash', -- Physical cash
'digital_wallet', -- PayMe, AlipayHK, Octopus, etc.
'loan', -- Loan/liability tracking
'other' -- Custom account type
)),
-- Currency & Balance (8 decimal precision)
currency TEXT NOT NULL, -- ISO 4217 code: HKD, USD, CNY, etc.
initial_balance TEXT NOT NULL DEFAULT '0.00000000',
current_balance TEXT NOT NULL DEFAULT '0.00000000',
-- Visual Customization
color TEXT DEFAULT '#3B82F6', -- Hex color for UI
icon TEXT DEFAULT 'wallet', -- Lucide icon name
sort_order INTEGER DEFAULT 0, -- Display order
-- Settings
is_active BOOLEAN DEFAULT 1, -- Archived accounts hidden by default
is_archived BOOLEAN DEFAULT 0,
include_in_net_worth BOOLEAN DEFAULT 1, -- Exclude loans from net worth
show_in_combined_view BOOLEAN DEFAULT 1, -- Show in dashboard
-- Sync & Audit
created_at TEXT NOT NULL, -- ISO 8601 UTC
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1, -- Increment on every change
device_id TEXT, -- Device that last modified
is_deleted BOOLEAN DEFAULT 0, -- Soft delete for sync
-- Constraints
CHECK (initial_balance GLOB '-?[0-9]*.?[0-9]*'),
CHECK (current_balance GLOB '-?[0-9]*.?[0-9]*')
);
Indexes:
CREATE INDEX idx_accounts_active ON accounts(is_active, is_archived);
CREATE INDEX idx_accounts_type ON accounts(account_type);
CREATE INDEX idx_accounts_currency ON accounts(currency);
2. tags
Flat tag-based categorization system.
CREATE TABLE tags (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE, -- "Food", "Transport", "Salary"
color TEXT NOT NULL, -- Hex color (full color picker)
icon TEXT, -- Lucide icon name
-- Budgeting (optional)
budget_amount TEXT, -- NULL = no budget
budget_period TEXT CHECK(budget_period IN ('daily', 'weekly', 'monthly', 'yearly')),
-- Flags
is_system BOOLEAN DEFAULT 0, -- Built-in tags (protected)
sort_order INTEGER DEFAULT 0,
-- Sync & Audit
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (budget_amount GLOB '-?[0-9]*.?[0-9]*' OR budget_amount IS NULL)
);
Indexes:
CREATE INDEX idx_tags_active ON tags(is_deleted);
CREATE INDEX idx_tags_system ON tags(is_system);
3. transactions
Core transaction table for expenses, incomes, and transfers.
CREATE TABLE transactions (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
-- Transaction Type
transaction_type TEXT NOT NULL CHECK(transaction_type IN (
'expense', -- Money out
'income', -- Money in
'transfer_out', -- Transfer from this account
'transfer_in' -- Transfer to this account
)),
-- Amounts (8 decimal precision)
gross_amount TEXT NOT NULL, -- Pre-tax amount
tax_amount TEXT DEFAULT '0.00000000',
net_amount TEXT NOT NULL, -- Post-tax (actual paid)
tax_rate TEXT, -- e.g., "0.00000000" for HK (no GST)
-- Currency
currency TEXT NOT NULL,
-- Description & Details
description TEXT NOT NULL,
merchant TEXT,
notes TEXT,
-- Receipt
receipt_paths JSON, -- Array of file paths: ["receipts/xxx.jpg"]
receipt_ocr_data JSON, -- Parsed OCR data
-- Transfer Linkage
transfer_id TEXT, -- Links paired transfer records
related_transaction_id TEXT REFERENCES transactions(id),
-- Schedule
schedule_id TEXT REFERENCES scheduled_transactions(id),
is_scheduled_instance BOOLEAN DEFAULT 0,
is_auto_inserted BOOLEAN DEFAULT 0, -- Flag for auto-inserted scheduled transactions
needs_review BOOLEAN DEFAULT 0, -- Review reminder flag
-- Dates (ISO 8601)
transaction_date TEXT NOT NULL, -- Date only: "2026-02-13"
created_at TEXT NOT NULL, -- Full datetime
updated_at TEXT NOT NULL,
-- Sync & Audit
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
sync_status TEXT DEFAULT 'synced' CHECK(sync_status IN ('synced', 'pending', 'conflict', 'error')),
-- Constraints
CHECK (gross_amount GLOB '-?[0-9]*.?[0-9]*'),
CHECK (tax_amount GLOB '-?[0-9]*.?[0-9]*'),
CHECK (net_amount GLOB '-?[0-9]*.?[0-9]*'),
CHECK (tax_rate GLOB '-?[0-9]*.?[0-9]*' OR tax_rate IS NULL),
-- Enforce positive amounts for expenses
CHECK (
(transaction_type IN ('expense', 'transfer_out') AND CAST(gross_amount AS REAL) > 0)
OR
(transaction_type IN ('income', 'transfer_in') AND CAST(gross_amount AS REAL) > 0)
)
);
Indexes:
CREATE INDEX idx_transactions_account ON transactions(account_id, is_deleted);
CREATE INDEX idx_transactions_date ON transactions(transaction_date DESC);
CREATE INDEX idx_transactions_type ON transactions(transaction_type);
CREATE INDEX idx_transactions_schedule ON transactions(schedule_id);
CREATE INDEX idx_transactions_transfer ON transactions(transfer_id);
CREATE INDEX idx_transactions_review ON transactions(needs_review) WHERE needs_review = 1;
CREATE INDEX idx_transactions_auto ON transactions(is_auto_inserted) WHERE is_auto_inserted = 1;
4. transaction_tags
Many-to-many relationship between transactions and tags.
CREATE TABLE transaction_tags (
transaction_id TEXT NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (transaction_id, tag_id)
);
Indexes:
CREATE INDEX idx_transaction_tags_tag ON transaction_tags(tag_id);
5. scheduled_transactions
Recurring transaction templates.
CREATE TABLE scheduled_transactions (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL REFERENCES accounts(id),
-- Schedule Pattern
schedule_type TEXT NOT NULL CHECK(schedule_type IN (
'daily',
'weekly', -- Select day(s) of week
'monthly', -- Select day of month
'yearly', -- Select month and day
'custom' -- Custom interval
)),
frequency INTEGER DEFAULT 1, -- Every N days/weeks/months
-- For weekly: 0=Sunday, 1=Monday, etc. (JSON array for multiple)
days_of_week JSON,
-- For monthly: 1-31 (-1 = last day of month)
day_of_month INTEGER,
-- For yearly: month (1-12)
month_of_year INTEGER,
-- Execution Time
execution_time TEXT DEFAULT '00:00', -- HH:MM format
timezone TEXT, -- User's timezone or explicit
-- Start/End Conditions
start_date TEXT NOT NULL,
end_date TEXT, -- NULL = indefinite
occurrence_count INTEGER, -- NULL = indefinite
current_occurrence INTEGER DEFAULT 0,
-- Transaction Template
transaction_type TEXT NOT NULL,
gross_amount TEXT NOT NULL,
tax_amount TEXT DEFAULT '0.00000000',
net_amount TEXT NOT NULL,
currency TEXT NOT NULL,
description TEXT,
merchant TEXT,
notes TEXT,
-- Tags (JSON array of tag IDs)
tag_ids JSON,
-- Status
is_active BOOLEAN DEFAULT 1,
last_generated_date TEXT,
next_execution_datetime TEXT, -- Full datetime in UTC
-- Sync & Audit
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (CAST(gross_amount AS REAL) > 0),
CHECK (frequency > 0)
);
Indexes:
CREATE INDEX idx_scheduled_next_exec ON scheduled_transactions(next_execution_datetime)
WHERE is_active = 1 AND is_deleted = 0;
CREATE INDEX idx_scheduled_account ON scheduled_transactions(account_id);
6. scheduled_instances
Track generated instances of scheduled transactions.
CREATE TABLE scheduled_instances (
id TEXT PRIMARY KEY,
schedule_id TEXT NOT NULL REFERENCES scheduled_transactions(id) ON DELETE CASCADE,
transaction_id TEXT REFERENCES transactions(id),
due_date TEXT NOT NULL, -- Date this instance was due
is_generated BOOLEAN DEFAULT 0,
is_skipped BOOLEAN DEFAULT 0,
generated_at TEXT,
notified BOOLEAN DEFAULT 0, -- Track if user was reminded
created_at TEXT NOT NULL
);
Indexes:
CREATE INDEX idx_scheduled_instances_schedule ON scheduled_instances(schedule_id);
CREATE INDEX idx_scheduled_instances_due ON scheduled_instances(due_date)
WHERE is_generated = 0 AND is_skipped = 0;
7. goals
Financial goals with progress tracking.
CREATE TABLE goals (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
-- Target
target_amount TEXT NOT NULL,
current_amount TEXT NOT NULL DEFAULT '0.00000000',
currency TEXT NOT NULL,
-- Type
goal_type TEXT NOT NULL CHECK(goal_type IN (
'savings', -- Save up to target
'debt_payoff', -- Pay off debt
'spending_limit', -- Don't exceed limit
'custom'
)),
-- Timeline
target_date TEXT, -- Optional deadline
is_recurring BOOLEAN DEFAULT 0, -- Reset monthly/quarterly?
recurrence_period TEXT CHECK(recurrence_period IN ('monthly', 'quarterly', 'yearly')),
-- Link to account (optional)
linked_account_id TEXT REFERENCES accounts(id),
-- Visual
color TEXT,
icon TEXT,
-- Status
is_active BOOLEAN DEFAULT 1,
is_achieved BOOLEAN DEFAULT 0,
achieved_at TEXT,
last_reset_date TEXT, -- For recurring goals
-- Sync & Audit
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (CAST(target_amount AS REAL) > 0),
CHECK (CAST(current_amount AS REAL) >= 0)
);
Indexes:
CREATE INDEX idx_goals_active ON goals(is_active, is_deleted);
CREATE INDEX idx_goals_achieved ON goals(is_achieved) WHERE is_achieved = 0;
CREATE INDEX idx_goals_account ON goals(linked_account_id);
8. goal_rules
Auto-contribution rules for goals.
CREATE TABLE goal_rules (
id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
-- Match Conditions
-- Match if transaction has ALL specified tags (AND logic)
tag_ids JSON NOT NULL, -- Array of tag IDs
-- Contribution Calculation
contribution_type TEXT NOT NULL CHECK(contribution_type IN ('percentage', 'fixed')),
percentage TEXT, -- e.g., "10.00" = 10%
fixed_amount TEXT, -- Fixed amount per transaction
-- Limits
max_contribution_per_transaction TEXT, -- Cap per transaction
monthly_cap TEXT, -- Monthly contribution limit
is_active BOOLEAN DEFAULT 1,
-- Sync & Audit
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (
(contribution_type = 'percentage' AND percentage IS NOT NULL)
OR
(contribution_type = 'fixed' AND fixed_amount IS NOT NULL)
),
CHECK (CAST(percentage AS REAL) >= 0 AND CAST(percentage AS REAL) <= 100)
);
Indexes:
CREATE INDEX idx_goal_rules_goal ON goal_rules(goal_id);
CREATE INDEX idx_goal_rules_active ON goal_rules(is_active);
9. goal_progress
Track contributions to goals.
CREATE TABLE goal_progress (
id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
amount TEXT NOT NULL, -- Amount added (can be negative for adjustments)
transaction_id TEXT REFERENCES transactions(id),
notes TEXT,
recorded_at TEXT NOT NULL,
-- Constraints
CHECK (amount GLOB '-?[0-9]*.?[0-9]*')
);
Indexes:
CREATE INDEX idx_goal_progress_goal ON goal_progress(goal_id);
CREATE INDEX idx_goal_progress_date ON goal_progress(recorded_at);
10. transfers
Track account-to-account transfers with FX rates.
CREATE TABLE transfers (
id TEXT PRIMARY KEY,
from_account_id TEXT NOT NULL REFERENCES accounts(id),
to_account_id TEXT NOT NULL REFERENCES accounts(id),
from_transaction_id TEXT REFERENCES transactions(id),
to_transaction_id TEXT REFERENCES transactions(id),
-- Amounts
from_amount TEXT NOT NULL, -- In from_account currency
to_amount TEXT NOT NULL, -- In to_account currency
exchange_rate TEXT, -- Rate used (to_amount / from_amount)
exchange_rate_source TEXT, -- 'auto', 'manual', 'api'
fees TEXT DEFAULT '0.00000000',
description TEXT,
transfer_date TEXT NOT NULL,
created_at TEXT NOT NULL,
-- Sync & Audit
version INTEGER DEFAULT 1,
device_id TEXT,
is_deleted BOOLEAN DEFAULT 0,
-- Constraints
CHECK (from_account_id != to_account_id),
CHECK (CAST(from_amount AS REAL) > 0),
CHECK (CAST(to_amount AS REAL) > 0)
);
Indexes:
CREATE INDEX idx_transfers_from ON transfers(from_account_id);
CREATE INDEX idx_transfers_to ON transfers(to_account_id);
CREATE INDEX idx_transfers_date ON transfers(transfer_date);
11. exchange_rates
Historical exchange rates for accurate conversions.
CREATE TABLE exchange_rates (
from_currency TEXT NOT NULL,
to_currency TEXT NOT NULL,
rate TEXT NOT NULL, -- 8 decimal precision
date TEXT NOT NULL, -- Date only
source TEXT, -- 'ECB', 'HKMA', 'manual', etc.
fetched_at TEXT, -- When rate was obtained
PRIMARY KEY (from_currency, to_currency, date),
CHECK (from_currency != to_currency),
CHECK (CAST(rate AS REAL) > 0)
);
Indexes:
CREATE INDEX idx_exchange_rates_date ON exchange_rates(date);
CREATE INDEX idx_exchange_rates_lookup ON exchange_rates(from_currency, to_currency, date DESC);
12. reconciliations
Simple account reconciliation records.
CREATE TABLE reconciliations (
id TEXT PRIMARY KEY,
account_id TEXT NOT NULL REFERENCES accounts(id),
statement_date TEXT NOT NULL, -- Statement period end
statement_balance TEXT NOT NULL, -- Balance per statement
app_balance TEXT NOT NULL, -- Calculated balance
difference TEXT NOT NULL, -- statement - app
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'balanced', 'adjusted')),
notes TEXT,
created_at TEXT NOT NULL,
resolved_at TEXT,
-- Sync & Audit
version INTEGER DEFAULT 1,
device_id TEXT,
CHECK (CAST(statement_balance AS REAL) IS NOT NULL),
CHECK (CAST(app_balance AS REAL) IS NOT NULL)
);
Indexes:
CREATE INDEX idx_reconciliations_account ON reconciliations(account_id);
CREATE INDEX idx_reconciliations_date ON reconciliations(statement_date DESC);
13. settings
Application settings key-value store.
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT NOT NULL
);
Default Settings:
INSERT INTO settings (key, value, updated_at) VALUES
('language', 'en', datetime('now')),
('default_currency', 'HKD', datetime('now')),
('base_currency', 'HKD', datetime('now')),
('timezone', 'auto', datetime('now')),
('default_view', 'combined', datetime('now')), -- 'combined' or 'single'
('display_mode', 'net', datetime('now')), -- 'net' or 'gross'
('decimal_places', '2', datetime('now')), -- Display precision
('date_format', 'YYYY-MM-DD', datetime('now')),
('time_format', '24h', datetime('now')),
('theme', 'system', datetime('now')), -- 'light', 'dark', 'system'
('week_starts_on', '1', datetime('now')), -- 0=Sun, 1=Mon
('scheduled_check_interval', '1', datetime('now')); -- Minutes
Database Initialization
-- Enable foreign keys
PRAGMA foreign_keys = ON;
-- Enable WAL mode for better concurrency
PRAGMA journal_mode = WAL;
-- Set synchronous mode for safety
PRAGMA synchronous = NORMAL;
-- Create all tables (in order)
-- ... (all CREATE TABLE statements above) ...
-- Insert default settings
-- ... (settings INSERT statements) ...
-- Insert default/system tags
INSERT INTO tags (id, name, color, icon, is_system, sort_order) VALUES
('tag-food', 'Food & Dining', '#EF4444', 'utensils', 1, 1),
('tag-transport', 'Transportation', '#3B82F6', 'car', 1, 2),
('tag-shopping', 'Shopping', '#8B5CF6', 'shopping-bag', 1, 3),
('tag-entertainment', 'Entertainment', '#F59E0B', 'film', 1, 4),
('tag-utilities', 'Utilities', '#10B981', 'zap', 1, 5),
('tag-health', 'Health & Medical', '#EC4899', 'heart-pulse', 1, 6),
('tag-education', 'Education', '#6366F1', 'graduation-cap', 1, 7),
('tag-salary', 'Salary', '#10B981', 'banknote', 1, 8),
('tag-investment', 'Investment', '#14B8A6', 'trending-up', 1, 9),
('tag-transfer', 'Transfer', '#6B7280', 'arrow-right-left', 1, 10);
Common Queries
Using SeaORM
Get Account Balance
use sea_orm::{entity::*, query::*};
use entities::account;
let balance: Option<String> = account::Entity::find_by_id(account_id)
.select_only()
.column(account::Column::CurrentBalance)
.into_tuple()
.one(&db)
.await?;
Get Transactions with Tags
use entities::{transaction, tag, transaction_tag};
let transactions = transaction::Entity::find()
.filter(transaction::Column::AccountId.eq(account_id))
.filter(transaction::Column::IsDeleted.eq(false))
.order_by_desc(transaction::Column::TransactionDate)
.find_with_linked(transaction::TagLink)
.all(&db)
.await?;
Get Spending by Tag (Monthly)
use sea_orm::{ sea_query::*, * };
let spending = transaction::Entity::find()
.select_only()
.column_as(tag::Column::Name, "tag_name")
.column_as(tag::Column::Color, "tag_color")
.column_as(transaction::Column::NetAmount.sum(), "total")
.inner_join(transaction_tag::Entity)
.inner_join(tag::Entity)
.filter(transaction::Column::TransactionType.eq("expense"))
.filter(transaction::Column::TransactionDate.gte("2026-02-01"))
.filter(transaction::Column::TransactionDate.lt("2026-03-01"))
.filter(transaction::Column::IsDeleted.eq(false))
.group_by(tag::Column::Id)
.order_by_desc(transaction::Column::NetAmount.sum())
.into_json()
.all(&db)
.await?;
Get Upcoming Scheduled Transactions
use entities::scheduled_transaction;
use chrono::Utc;
let now = Utc::now();
let upcoming = scheduled_transaction::Entity::find()
.filter(scheduled_transaction::Column::IsActive.eq(true))
.filter(scheduled_transaction::Column::IsDeleted.eq(false))
.filter(scheduled_transaction::Column::NextExecutionDatetime.lte(now))
.order_by_asc(scheduled_transaction::Column::NextExecutionDatetime)
.all(&db)
.await?;
Get Goal Progress
use entities::goal;
let goals = goal::Entity::find()
.filter(goal::Column::IsActive.eq(true))
.filter(goal::Column::IsDeleted.eq(false))
.order_by_desc(goal::Column::CreatedAt)
.all(&db)
.await?;
// Calculate progress in Rust
for goal in goals {
let current: f64 = goal.current_amount.parse()?;
let target: f64 = goal.target_amount.parse()?;
let progress_percent = (current / target) * 100.0;
}
Raw SQL (if needed)
For complex queries, SeaORM supports raw SQL:
use sea_orm::{ConnectionTrait, Statement};
let stmt = Statement::from_sql_and_values(
db.get_database_backend(),
r#"
SELECT
tg.name,
tg.color,
SUM(CAST(t.net_amount AS REAL)) as total
FROM transactions t
JOIN transaction_tags tt ON t.id = tt.transaction_id
JOIN tags tg ON tt.tag_id = tg.id
WHERE t.transaction_type = 'expense'
AND strftime('%Y-%m', t.transaction_date) = ?
AND t.is_deleted = 0
GROUP BY tg.id
ORDER BY total DESC
"#,
["2026-02".into()]
);
let results = db.query_all(stmt).await?;
Data Integrity with SeaORM
Auto-Updating Timestamps and Version
With SeaORM, we handle these in the ActiveModel before updating:
// In your service/update function
pub async fn update_account(
db: &DatabaseConnection,
id: &str,
updates: AccountUpdate,
) -> Result<account::Model, DbErr> {
let account = account::Entity::find_by_id(id)
.one(db)
.await?
.ok_or(DbErr::Custom("Account not found".to_string()))?;
let mut active_model: account::ActiveModel = account.into();
// Update fields
if let Some(name) = updates.name {
active_model.name = Set(name);
}
// Auto-update audit fields
active_model.updated_at = Set(Utc::now());
active_model.version = Set(active_model.version.unwrap() + 1);
active_model.update(db).await
}
Database Constraints
SeaORM supports check constraints in migrations:
// In migration file
.col(ColumnDef::new(Account::InitialBalance)
.string()
.not_null()
.check(Expr::col(Account::InitialBalance).like("-?[0-9]*.?[0-9]*")))
Migration Strategy with SeaORM
SeaORM provides a built-in migration system:
Migration Structure
src-tauri/src/migrations/
├── mod.rs
├── m001_initial.rs
├── m002_add_user_preferences.rs
└── m003_add_indexes.rs
Migration Files
// src-tauri/src/migrations/mod.rs
pub use sea_orm_migration::prelude::*;
mod m001_initial;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m001_initial::Migration),
]
}
}
Running Migrations
// In main.rs or lib.rs
use migrations::Migrator;
use sea_orm_migration::MigratorTrait;
pub async fn run_migrations(db: &DatabaseConnection) -> Result<(), DbErr> {
Migrator::up(db, None).await
}
Migration Best Practices
- Each migration in separate file:
m001_initial.rs,m002_... - Both up() and down(): Always implement rollback
- Test migrations: Test both up and down on sample data
- Never modify existing migrations after deployment
- Always backup before migration
- Handle data migration: Use raw SQL for complex data transformations
Checking Migration Status
# Check status
sea-orm-cli migrate status
# Run pending migrations
sea-orm-cli migrate up
# Rollback last migration
sea-orm-cli migrate down
# Rollback all migrations
sea-orm-cli migrate down -n 999
Backup Format (JSON Export)
{
"version": "1.0.0",
"exported_at": "2026-02-13T10:30:00Z",
"schema_version": 1,
"data": {
"accounts": [...],
"tags": [...],
"transactions": [...],
"transaction_tags": [...],
"scheduled_transactions": [...],
"scheduled_instances": [...],
"goals": [...],
"goal_rules": [...],
"goal_progress": [...],
"transfers": [...],
"exchange_rates": [...],
"reconciliations": [...],
"settings": [...]
},
"checksum": "sha256_hash"
}
Performance Considerations
- Indexes: All foreign keys and frequently queried columns indexed
- Pagination: Use LIMIT/OFFSET for transaction lists
- Lazy loading: Load receipts on demand
- Caching: Cache account balances, don't calculate on every read
- Archiving: Soft delete instead of hard delete for sync compatibility
Security
- Encryption: Optional SQLCipher for database encryption
- Key storage: Use OS keychain (iOS Keychain, Android Keystore)
- Receipts: Store locally, no cloud upload
- No network: Except OCR API calls (opt-in)