Georm
A simple, type-safe SQLx ORM for PostgreSQL
## Overview
Georm is a lightweight, opinionated Object-Relational Mapping (ORM) library built on top of [SQLx](https://crates.io/crates/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
- **Composite Primary Keys**: Support for multi-field primary keys
- **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`:
```toml
[dependencies]
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "macros"] }
georm = "0.1"
```
### Basic Usage
1. **Define your database schema**:
```sql
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()
);
```
2. **Define your Rust entities**:
```rust
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,
}
```
3. **Use the generated methods**:
```rust
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
### Composite Primary Keys
Georm supports composite primary keys by marking multiple fields with `#[georm(id)]`:
```rust
#[derive(Georm)]
#[georm(table = "user_roles")]
pub struct UserRole {
#[georm(id)]
pub user_id: i32,
#[georm(id)]
pub role_id: i32,
pub assigned_at: chrono::DateTime,
}
```
This automatically generates a composite ID struct:
```rust
// Generated automatically
pub struct UserRoleId {
pub user_id: i32,
pub role_id: i32,
}
// Usage
let id = UserRoleId { user_id: 1, role_id: 2 };
let user_role = UserRole::find(pool, &id).await?;
```
**Note**: Relationships are not yet supported for entities with composite primary keys.
### Defaultable and Generated Fields
Georm provides three attributes for handling fields with database-managed values:
#### `#[georm(defaultable)]` - Optional Override Fields
For fields with database defaults that can be manually overridden:
```rust
#[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, // DEFAULT NOW()
pub author_id: i32,
}
```
#### `#[georm(generated)]` - Generated by Default
For PostgreSQL `GENERATED BY DEFAULT` columns that can be overridden but are typically auto-generated:
```rust
#[derive(Georm)]
#[georm(table = "products")]
pub struct Product {
#[georm(id, generated_always)]
pub id: i32, // GENERATED ALWAYS AS IDENTITY
#[georm(generated)]
pub sku_number: i32, // GENERATED BY DEFAULT AS IDENTITY
pub name: String,
pub price: sqlx::types::BigDecimal,
}
```
#### `#[georm(generated_always)]` - Always Generated
For PostgreSQL `GENERATED ALWAYS` columns that are strictly managed by the database:
```rust
#[derive(Georm)]
#[georm(table = "products")]
pub struct Product {
#[georm(id, generated_always)]
pub id: i32, // GENERATED ALWAYS AS IDENTITY
pub name: String,
pub price: sqlx::types::BigDecimal,
pub discount_percent: i32,
#[georm(generated_always)]
pub final_price: Option, // GENERATED ALWAYS AS (expression) STORED
}
```
#### Generated Structs and Behavior
Both `defaultable` and `generated` fields create a companion `Default` struct:
```rust
use georm::Defaultable;
let product_default = ProductDefault {
name: "Laptop".to_string(),
price: BigDecimal::from(999),
discount_percent: 10,
sku_number: None, // Let database auto-generate
// Note: generated_always fields are excluded from this struct
};
let created_product = product_default.create(pool).await?;
```
#### Key Differences
| Attribute | INSERT Behavior | UPDATE Behavior | Use Case |
|--------------------|------------------------------------|-----------------|----------------------------------------------|
| `defaultable` | Optional (can override defaults) | Included | Fields with database defaults |
| `generated` | Optional (can override generation) | Included | `GENERATED BY DEFAULT` columns |
| `generated_always` | **Excluded** (always generated) | **Excluded** | `GENERATED ALWAYS` columns, computed columns |
#### Important Notes
- **`generated_always` fields are completely excluded** from INSERT and UPDATE statements to prevent database errors
- **`generated` and `generated_always` cannot be used together** on the same field
- **`generated` fields behave like `defaultable` fields** but are semantically distinct for future enhancements
- **Option types cannot be marked as `defaultable`** to prevent `Option