diff --git a/LICENSE.gpl-3.0.md b/LICENSE.GPL.md
similarity index 100%
rename from LICENSE.gpl-3.0.md
rename to LICENSE.GPL.md
diff --git a/README.md b/README.md
index 3bcbac5..61d63df 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
Georm
- A simple, opinionated SQLx ORM for PostgreSQL
+ A simple, type-safe SQLx ORM for PostgreSQL
@@ -15,7 +15,8 @@
-## What is Georm?
+## Overview
-Georm is a quite simple ORM built around
-[SQLx](https://crates.io/crates/sqlx) that gives access to a few
-useful functions when interacting with a database, implementing
-automatically the most basic SQL interactions you’re tired of writing.
+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.
-## Why is Georm?
+### Key Features
-I wanted an ORM that’s easy and straightforward to use. I am aware
-some other projects exist, such as
-[SeaORM](https://www.sea-ql.org/SeaORM/), but they generally don’t fit
-my needs and/or my wants of a simple interface. I ended up writing the
-ORM I wanted to use.
+- **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
-## How is Georm?
+## Quick Start
-I use it in a few projects, and I’m quite happy with it right now. But
-of course, I’m open to constructive criticism and suggestions!
+### Installation
-## How can I use it?
+Add Georm and SQLx to your `Cargo.toml`:
-Georm works with SQLx, but does not re-export it itself. To get
-started, install both Georm and SQLx in your Rust project:
-
-```sh
-cargo add sqlx --features postgres,macros # and any other feature you might want
-cargo add georm
+```toml
+[dependencies]
+sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "macros"] }
+georm = "0.1"
```
-As Georm relies heavily on the macro
-[`query_as!`](https://docs.rs/sqlx/latest/sqlx/macro.query_as.html),
-the `macros` feature is not optional. Declare your tables in your
-Postgres database (you may want to use SQLx’s `migrate` feature for
-this), and then declare their equivalent in Rust.
+### Basic Usage
+
+1. **Define your database schema**:
```sql
-CREATE TABLE biographies (
- id SERIAL PRIMARY KEY,
- content TEXT NOT NULL
-);
-
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
- biography_id INT,
- FOREIGN KEY (biography_id) REFERENCES biographies(id)
+ 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()
);
```
-```rust
-pub struct Author {
- pub id: i32,
- pub name: String,
-}
-```
+2. **Define your Rust entities**:
-To link a struct to a table in your database, derive the
-`sqlx::FromRow` and the `georm::Georm` traits.
```rust
-#[derive(sqlx::FromRow, Georm)]
-pub struct Author {
- pub id: i32,
- pub name: String,
-}
-```
+use georm::Georm;
-Now, indicate with the `georm` proc-macro which table they refer to.
-```rust
#[derive(sqlx::FromRow, Georm)]
#[georm(table = "authors")]
pub struct Author {
+ #[georm(id)]
pub id: i32,
pub name: String,
+ pub email: String,
+}
+
+#[derive(sqlx::FromRow, 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,
}
```
-Finally, indicate with the same proc-macro which field of your struct
-is the primary key in your database.
+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
+
+### Defaultable Fields
+
+For fields with database defaults or auto-generated values, use the `defaultable` attribute:
+
```rust
#[derive(sqlx::FromRow, Georm)]
-#[georm(table = "authors")]
+#[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,
+}
+```
+
+This generates a `PostDefault` struct for easier creation:
+
+```rust
+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:
+
+```rust
+#[derive(sqlx::FromRow, 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:
+
+```rust
+#[derive(sqlx::FromRow, 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,
+}
+```
+
+**Generated method**: `post.get_category(pool).await? -> Option`
+
+#### Struct-Level Relationships (Reverse Lookups)
+
+Define relationships at the struct level to query related entities that reference this entity:
+
+##### One-to-One Relationships
+
+```rust
+#[derive(sqlx::FromRow, 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`
+
+##### One-to-Many Relationships
+
+```rust
+#[derive(sqlx::FromRow, 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,
@@ -115,83 +283,429 @@ pub struct Author {
}
```
-Congratulations, your struct `Author` now has access to all the
-functions described in the `Georm` trait!
+**Generated methods**:
+- `author.get_posts(pool).await? -> Vec`
+- `author.get_comments(pool).await? -> Vec`
-## Entity relationship
+##### Many-to-Many Relationships
+
+For many-to-many relationships, specify the link table that connects the entities:
+
+```sql
+-- 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)
+);
+```
-It is possible to implement one-to-one, one-to-many, and many-to-many
-relationships with Georm. This is a quick example of how a struct with
-several relationships of different types may be declared:
```rust
#[derive(sqlx::FromRow, Georm)]
#[georm(
table = "books",
- one_to_one = [
- { name = "draft", remote_id = "book_id", table = "drafts", entity = Draft }
- ],
- one_to_many = [
- { name = "reviews", remote_id = "book_id", table = "reviews", entity = Review },
- { name = "reprints", remote_id = "book_id", table = "reprints", entity = Reprint }
- ],
many_to_many = [{
- name = "genres",
- table = "genres",
- entity = Genre,
- link = { table = "book_genres", from = "book_id", to = "genre_id" }
+ 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)]
- ident: i32,
- title: String,
- #[georm(relation = {entity = Author, table = "authors", name = "author"})]
- author_id: i32,
+ pub id: i32,
+ pub title: String,
+}
+
+#[derive(sqlx::FromRow, 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,
}
```
-To read more about these features, you can refer to the [online
-documentation](https://docs.rs/georm/).
+**Generated methods**:
+- `book.get_genres(pool).await? -> Vec`
+- `genre.get_books(pool).await? -> Vec`
-## Roadmap / TODO
+#### Relationship Attribute Reference
-The following features are being considered for future development:
+| 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:
+
+```rust
+#[derive(sqlx::FromRow, 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,
+}
+```
+
+**Generated methods**:
+- `post.get_author(pool).await? -> Author` (from field relation)
+- `post.get_category(pool).await? -> Option` (nullable field relation)
+- `post.get_comments(pool).await? -> Vec` (one-to-many)
+- `post.get_tags(pool).await? -> Vec` (many-to-many)
+
+## API Reference
+
+### Core Operations
+
+All entities implementing `Georm` get these methods:
+
+```rust
+// 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 `Default` struct:
+
+```rust
+// Create with defaults
+post_default.create(pool).await?;
+```
+
+## Configuration
+
+### Attributes Reference
+
+#### Struct-level attributes
+
+```rust
+#[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
+
+```rust
+#[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
+
+## 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**: Add comprehensive transaction support with
- transaction-aware CRUD methods and relationship handling for atomic
- operations across multiple entities
-- **Race Condition Fix**: Replace the current `create_or_update`
- implementation with database-specific UPSERT operations (PostgreSQL
- `ON CONFLICT`, MySQL `ON DUPLICATE KEY UPDATE`, SQLite `ON
- CONFLICT`) to prevent race conditions
+- **Transaction Support**: Comprehensive transaction handling with atomic operations
+- **Race Condition Fix**: Database-native UPSERT operations to replace current `create_or_update`
### Medium Priority
-- **Multi-Database Support**: Extend Georm to support MySQL and SQLite
- in addition to PostgreSQL, with database-specific optimizations and
- dialect handling
-- **Relationship Optimization**: Implement eager loading and N+1 query
- prevention with circular dependency protection to dramatically
- improve performance when working with related entities
-- **Composite Primary Keys**: Add support for entities with multiple
- primary key fields using auto-generated ID structs and type-safe
- composite key handling
-- **Soft Delete**: Implement optional soft delete functionality with
- `deleted_at` timestamps, allowing entities to be marked as deleted
- without physical removal
+- **Multi-Database Support**: MySQL and SQLite support with feature flags
+- **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**: Add optional migration utilities that
- leverage SQLx's existing infrastructure for schema generation,
- verification, and evolution
-- **Enhanced Error Handling**: Consider implementing custom error
- types with better categorization and operation context while
- maintaining compatibility with SQLx errors
-- **Many-to-Many Relationship Improvements**: Add direct methods to
- add or remove items from many-to-many relationships without manually
- handling the join table
+- **Migration Support**: Schema generation and evolution utilities
+- **Enhanced Error Handling**: Custom error types with better context
-### Recently Completed
-- ✅ **Defaultable Fields**: Support for fields with database defaults
- or auto-generated values, creating companion structs with optional
- fields for easier entity creation
+## Contributing
+
+We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
+
+### Development Setup
+
+#### Prerequisites
+
+- **Rust 1.81+**: 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](https://github.com/casey/just)**: Task runner for common development commands
+- **[cargo-deny](https://github.com/EmbarkStudios/cargo-deny)**: License and security auditing
+- **[sqlx-cli](https://github.com/launchbadge/sqlx/tree/main/sqlx-cli)**: Database migrations and management
+- **[bacon](https://github.com/Canop/bacon)**: Background code checker (optional but recommended)
+
+Install these tools:
+
+```bash
+# 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
+
+```bash
+# 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)
+
+```bash
+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
+
+```bash
+# 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:
+
+```bash
+# Run clippy continuously
+bacon
+
+# Run tests continuously
+bacon test
+
+# Build docs continuously
+bacon doc
+```
+
+#### Nix Development Environment (Optional)
+
+If you use [Nix](https://nixos.org/), you can use the provided flake for a reproducible development environment:
+
+```bash
+# Enter the development shell with all tools pre-installed
+nix develop
+
+# Or use direnv for automatic environment activation
+direnv allow
+```
+
+The Nix flake provides:
+- Exact Rust version (1.81) with required components
+- All development tools (just, cargo-deny, sqlx-cli, bacon)
+- LSP support (rust-analyzer)
+- SQL tooling (sqls for SQL language server)
+
+**Nix flake contents:**
+- **Rust toolchain**: Specified version with rustfmt, clippy, and rust-analyzer
+- **Development tools**: just, cargo-deny, sqlx-cli, bacon
+- **SQL tools**: sqls (SQL language server)
+- **Platform support**: Currently x86_64-linux (can be extended)
+
+#### Database Setup for Tests
+
+Tests require a PostgreSQL database. Set up a test database:
+
+```sql
+-- 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:
+
+```bash
+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:
+
+```bash
+# Format code
+just format
+
+# Check formatting (CI)
+just format-check
+```
+
+Clippy linting is enforced:
+
+```bash
+# Run linting
+just lint
+
+# Fix auto-fixable lints
+cargo clippy --fix
+```
+
+## License
+
+Licensed under either of
+
+ * MIT License ([LICENSE-MIT](LICENSE-MIT.md) or http://opensource.org/licenses/MIT)
+ * GNU General Public License v3.0 ([LICENSE-GPL](LICENSE-GPL.md) or https://www.gnu.org/licenses/gpl-3.0.html)
+
+at your option.
+
+## Acknowledgments
+
+- Built on top of the excellent [SQLx](https://github.com/launchbadge/sqlx) library
+- Inspired by [Hibernate](https://hibernate.org/)