Adds an example demonstrating user, comment, and follower relationship including: - User management with profiles - Comments (not really useful, just for showcasing) - Follower/follozing relationships - Ineractive CLI interface with CRUD operations - Database migrations for the example schema
20 KiB
Georm
Overview
Georm is a lightweight, opinionated Object-Relational Mapping (ORM) library built on top of SQLx for PostgreSQL. It provides a clean, type-safe interface for common database operations while leveraging SQLx's compile-time query verification.
Key Features
- Type Safety: Compile-time verified SQL queries using SQLx macros
- Zero Runtime Cost: No reflection or runtime query building
- Simple API: Intuitive derive macros for common operations
- Relationship Support: One-to-one, one-to-many, and many-to-many relationships
- Defaultable Fields: Easy entity creation with database defaults and auto-generated values
- PostgreSQL Native: Optimized for PostgreSQL features and data types
Quick Start
Installation
Add Georm and SQLx to your Cargo.toml
:
[dependencies]
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "macros"] }
georm = "0.1"
Basic Usage
- Define your database schema:
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
published BOOLEAN DEFAULT FALSE,
author_id INT NOT NULL REFERENCES authors(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
- Define your Rust entities:
use georm::Georm;
#[derive(Georm)]
#[georm(table = "authors")]
pub struct Author {
#[georm(id)]
pub id: i32,
pub name: String,
pub email: String,
}
#[derive(Georm)]
#[georm(table = "posts")]
pub struct Post {
#[georm(id)]
pub id: i32,
pub title: String,
pub content: String,
pub published: bool,
#[georm(relation = {
entity = Author,
table = "authors",
name = "author"
})]
pub author_id: i32,
pub created_at: chrono::DateTime<chrono::Utc>,
}
- Use the generated methods:
use sqlx::PgPool;
async fn example(pool: &PgPool) -> sqlx::Result<()> {
// Create an author
let author = Author {
id: 0, // Will be auto-generated
name: "Jane Doe".to_string(),
email: "jane@example.com".to_string(),
};
let author = author.create(pool).await?;
// Create a post
let post = Post {
id: 0,
title: "Hello, Georm!".to_string(),
content: "This is my first post using Georm.".to_string(),
published: false,
author_id: author.id,
created_at: chrono::Utc::now(),
};
let post = post.create(pool).await?;
// Find all posts
let all_posts = Post::find_all(pool).await?;
// Get the post's author
let post_author = post.get_author(pool).await?;
println!("Post '{}' by {}", post.title, post_author.name);
Ok(())
}
Advanced Features
Defaultable Fields
For fields with database defaults or auto-generated values, use the defaultable
attribute:
#[derive(Georm)]
#[georm(table = "posts")]
pub struct Post {
#[georm(id, defaultable)]
pub id: i32, // Auto-generated serial
pub title: String,
#[georm(defaultable)]
pub published: bool, // Has database default (false)
#[georm(defaultable)]
pub created_at: chrono::DateTime<chrono::Utc>, // DEFAULT NOW()
pub author_id: i32,
}
This generates a PostDefault
struct for easier creation:
use georm::Defaultable;
let post_default = PostDefault {
id: None, // Let database auto-generate
title: "My Post".to_string(),
published: None, // Use database default
created_at: None, // Use database default (NOW())
author_id: 42,
};
let created_post = post_default.create(pool).await?;
Relationships
Georm supports comprehensive relationship modeling with two approaches: field-level relationships for foreign keys and struct-level relationships for reverse lookups.
Field-Level Relationships (Foreign Keys)
Use the relation
attribute on foreign key fields to generate lookup methods:
#[derive(Georm)]
#[georm(table = "posts")]
pub struct Post {
#[georm(id)]
pub id: i32,
pub title: String,
#[georm(relation = {
entity = Author, // Target entity type
table = "authors", // Target table name
name = "author", // Method name (generates get_author)
remote_id = "id", // Target table's key column (default: "id")
nullable = false // Whether relationship can be null (default: false)
})]
pub author_id: i32,
}
Generated method: post.get_author(pool).await? -> Author
For nullable relationships:
#[derive(Georm)]
#[georm(table = "posts")]
pub struct Post {
#[georm(id)]
pub id: i32,
pub title: String,
#[georm(relation = {
entity = Category,
table = "categories",
name = "category",
nullable = true // Allows NULL values
})]
pub category_id: Option<i32>,
}
Generated method: post.get_category(pool).await? -> Option<Category>
Struct-Level Relationships (Reverse Lookups)
Define relationships at the struct level to query related entities that reference this entity:
One-to-One Relationships
#[derive(Georm)]
#[georm(
table = "users",
one_to_one = [{
entity = Profile, // Related entity type
name = "profile", // Method name (generates get_profile)
table = "profiles", // Related table name
remote_id = "user_id", // Foreign key in related table
}]
)]
pub struct User {
#[georm(id)]
pub id: i32,
pub username: String,
}
Generated method: user.get_profile(pool).await? -> Option<Profile>
One-to-Many Relationships
#[derive(Georm)]
#[georm(
table = "authors",
one_to_many = [{
entity = Post, // Related entity type
name = "posts", // Method name (generates get_posts)
table = "posts", // Related table name
remote_id = "author_id" // Foreign key in related table
}, {
entity = Comment, // Multiple relationships allowed
name = "comments",
table = "comments",
remote_id = "author_id"
}]
)]
pub struct Author {
#[georm(id)]
pub id: i32,
pub name: String,
}
Generated methods:
author.get_posts(pool).await? -> Vec<Post>
author.get_comments(pool).await? -> Vec<Comment>
Many-to-Many Relationships
For many-to-many relationships, specify the link table that connects the entities:
-- Example schema for books and genres
CREATE TABLE books (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL
);
CREATE TABLE genres (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE book_genres (
book_id INT NOT NULL REFERENCES books(id),
genre_id INT NOT NULL REFERENCES genres(id),
PRIMARY KEY (book_id, genre_id)
);
#[derive(Georm)]
#[georm(
table = "books",
many_to_many = [{
entity = Genre, // Related entity type
name = "genres", // Method name (generates get_genres)
table = "genres", // Related table name
remote_id = "id", // Primary key in related table (default: "id")
link = { // Link table configuration
table = "book_genres", // Join table name
from = "book_id", // Column referencing this entity
to = "genre_id" // Column referencing related entity
}
}]
)]
pub struct Book {
#[georm(id)]
pub id: i32,
pub title: String,
}
#[derive(Georm)]
#[georm(
table = "genres",
many_to_many = [{
entity = Book,
name = "books",
table = "books",
link = {
table = "book_genres",
from = "genre_id", // Note: reversed perspective
to = "book_id"
}
}]
)]
pub struct Genre {
#[georm(id)]
pub id: i32,
pub name: String,
}
Generated methods:
book.get_genres(pool).await? -> Vec<Genre>
genre.get_books(pool).await? -> Vec<Book>
Relationship Attribute Reference
Attribute | Description | Required | Default |
---|---|---|---|
entity |
Target entity type | Yes | N/A |
name |
Method name (generates get_{name} ) |
Yes | N/A |
table |
Target table name | Yes | N/A |
remote_id |
Target table's key column | No | "id" |
nullable |
Whether relationship can be null (field-level only) | No | false |
link.table |
Join table name (many-to-many only) | Yes* | N/A |
link.from |
Column referencing this entity (many-to-many only) | Yes* | N/A |
link.to |
Column referencing target entity (many-to-many only) | Yes* | N/A |
*Required for many-to-many relationships
Complex Relationship Example
Here's a comprehensive example showing multiple relationship types:
#[derive(Georm)]
#[georm(
table = "posts",
one_to_many = [{
entity = Comment,
name = "comments",
table = "comments",
remote_id = "post_id"
}],
many_to_many = [{
entity = Tag,
name = "tags",
table = "tags",
link = {
table = "post_tags",
from = "post_id",
to = "tag_id"
}
}]
)]
pub struct Post {
#[georm(id)]
pub id: i32,
pub title: String,
pub content: String,
// Field-level relationship (foreign key)
#[georm(relation = {
entity = Author,
table = "authors",
name = "author"
})]
pub author_id: i32,
// Nullable field-level relationship
#[georm(relation = {
entity = Category,
table = "categories",
name = "category",
nullable = true
})]
pub category_id: Option<i32>,
}
Generated methods:
post.get_author(pool).await? -> Author
(from field relation)post.get_category(pool).await? -> Option<Category>
(nullable field relation)post.get_comments(pool).await? -> Vec<Comment>
(one-to-many)post.get_tags(pool).await? -> Vec<Tag>
(many-to-many)
API Reference
Core Operations
All entities implementing Georm<Id>
get these methods:
// Query operations
Post::find_all(pool).await?; // Find all posts
Post::find(pool, &post_id).await?; // Find by ID
// Mutation operations
post.create(pool).await?; // Insert new record
post.update(pool).await?; // Update existing record
post.create_or_update(pool).await?; // Upsert operation
post.delete(pool).await?; // Delete this record
Post::delete_by_id(pool, &post_id).await?; // Delete by ID
// Utility
post.get_id(); // Get entity ID
Defaultable Operations
Entities with defaultable fields get a companion <Entity>Default
struct:
// Create with defaults
post_default.create(pool).await?;
Configuration
Attributes Reference
Struct-level attributes
#[georm(
table = "table_name", // Required: database table name
one_to_one = [{ /* ... */ }], // Optional: one-to-one relationships
one_to_many = [{ /* ... */ }], // Optional: one-to-many relationships
many_to_many = [{ /* ... */ }] // Optional: many-to-many relationships
)]
Field-level attributes
#[georm(id)] // Mark as primary key
#[georm(defaultable)] // Mark as defaultable field
#[georm(relation = { /* ... */ })] // Define relationship
Performance
Georm is designed for zero runtime overhead:
- Compile-time queries: All SQL is verified at compile time
- No reflection: Direct field access, no runtime introspection
- Minimal allocations: Efficient use of owned vs borrowed data
- SQLx integration: Leverages SQLx's optimized PostgreSQL driver
Examples
Comprehensive Example
For an example showcasing user management, comments, and follower relationships, see the example in examples/postgres/users-comments-and-followers/
. This example demonstrates:
- User management and profile management
- Comment system with user associations
- Follower/following relationships (many-to-many)
- Interactive CLI interface with CRUD operations
- Database migrations and schema setup
To run the example:
# Set up your database
export DATABASE_URL="postgres://username:password@localhost/georm_example"
# Run migrations
cargo sqlx migrate run
# Run the example
cd examples/postgres/users-comments-and-followers
cargo run help # For a list of all available actions
Comparison
Feature | Georm | SeaORM | Diesel |
---|---|---|---|
Compile-time safety | ✅ | ✅ | ✅ |
Relationship support | ✅ | ✅ | ✅ |
Async support | ✅ | ✅ | ⚠️ |
Learning curve | Low | Medium | High |
Macro simplicity | ✅ | ❌ | ❌ |
Advanced queries | ❌ | ✅ | ✅ |
Roadmap
High Priority
- Transaction Support: Comprehensive transaction handling with atomic operations
Medium Priority
- Multi-Database Support: MySQL and SQLite support with feature flags
- Field-Based Queries: Generate
find_by_{field_name}
methods that returnVec<T>
for regular fields orOption<T>
for unique fields - Relationship Optimization: Eager loading and N+1 query prevention
- Composite Primary Keys: Multi-field primary key support
- Soft Delete: Optional soft delete with
deleted_at
timestamps
Lower Priority
- Migration Support: Schema generation and evolution utilities
- Enhanced Error Handling: Custom error types with better context
Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
Prerequisites
- Rust 1.86+: Georm uses modern Rust features and follows the MSRV specified in
rust-toolchain.toml
- PostgreSQL 12+: Required for running tests and development
- Git: For version control
- Jujutsu: For version control (alternative to Git)
Required Tools
The following tools are used in the development workflow:
- just: Task runner for common development commands
- cargo-deny: License and security auditing
- sqlx-cli: Database migrations and management
- bacon: Background code checker (optional but recommended)
Install these tools:
# Install just (task runner)
cargo install just
# Install cargo-deny (for auditing)
cargo install cargo-deny
# Install sqlx-cli (for database management)
cargo install sqlx-cli --no-default-features --features native-tls,postgres
# Install bacon (optional, for live feedback)
cargo install bacon
Quick Start
# Clone the repository
git clone https://github.com/Phundrak/georm.git
cd georm
# Set up your PostgreSQL database and set DATABASE_URL
export DATABASE_URL="postgres://username:password@localhost/georm_test"
# Run migrations
just migrate
# Run all tests
just test
# Run linting
just lint
# Run security audit
just audit
# Run all checks (format, lint, audit, test)
just check-all
Available Commands (via just)
just # Default: run linting
just build # Build the project
just build-release # Build in release mode
just test # Run all tests
just lint # Run clippy linting
just audit # Run security and license audit
just migrate # Run database migrations
just format # Format all code
just format-check # Check code formatting
just check-all # Run all checks (format, lint, audit, test)
just clean # Clean build artifacts
Running Specific Tests
# Run tests for a specific module
cargo test --test simple_struct
cargo test --test defaultable_struct
cargo test --test m2m_relationship
# Run tests with output
cargo test -- --nocapture
# Run a specific test function
cargo test defaultable_struct_should_exist
Development with Bacon (Optional)
For continuous feedback during development:
# Run clippy continuously
bacon
# Run tests continuously
bacon test
# Build docs continuously
bacon doc
Devenv Development Environment (Optional)
If you use Nix, you can use the provided devenv configuration for a reproducible development environment:
# Enter the development shell with all tools pre-installed
devenv shell
# Or use direnv for automatic environment activation
direnv allow
The devenv configuration provides:
- Exact Rust version (1.86) with required components
- All development tools (just, cargo-deny, sqlx-cli, bacon)
- LSP support (rust-analyzer)
- SQL tooling (sqls for SQL language server)
- PostgreSQL database for development
Devenv configuration:
- Rust toolchain: Specified version with rustfmt, clippy, and rust-analyzer
- Development tools: just, cargo-deny, sqlx-cli, bacon
- SQL tools: sqls (SQL language server)
- Database: PostgreSQL with automatic setup
- Platform support: Cross-platform (Linux, macOS, etc.)
Database Setup for Tests
Tests require a PostgreSQL database. Set up a test database:
-- Connect to PostgreSQL as superuser
CREATE DATABASE georm_test;
CREATE USER georm_user WITH PASSWORD 'georm_password';
GRANT ALL PRIVILEGES ON DATABASE georm_test TO georm_user;
Set the environment variable:
export DATABASE_URL="postgres://georm_user:georm_password@localhost/georm_test"
IDE Setup
- Ensure
rust-analyzer
is configured - Set up PostgreSQL connection for SQL syntax highlighting
Code Style
The project uses standard Rust formatting:
# Format code
just format
# Check formatting (CI)
just format-check
Clippy linting is enforced:
# Run linting
just lint
# Fix auto-fixable lints
cargo clippy --fix
License
Licensed under either of
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- GNU General Public License v3.0 (LICENSE-GPL or https://www.gnu.org/licenses/gpl-3.0.html)
at your option.