mirror of
https://github.com/Phundrak/georm.git
synced 2025-07-29 14:05:35 +00:00
docs: rewrite documentation for core traits and library
Completely rewrite and expand documentation for Georm’s core functionality with detailed explanations, examples, and implementation details. Changes: - Rewrite lib.rs with comprehensive library documentation covering all features - Add extensive Georm trait documentation with method-specific details - Add detailed Defaultable trait documentation with usage patterns The documentation now provides complete coverage of: - All CRUD operations with database behavior details - Composite key support and generated ID structs - Defaultable field patterns and companion struct generation - Relationship modeling (field-level and struct-level) - Error handling and performance characteristics - PostgreSQL-specific features and optimizations
This commit is contained in:
parent
19284665e6
commit
a7696270da
@ -1,10 +1,278 @@
|
||||
/// Trait for creating entities with database defaults and auto-generated values.
|
||||
///
|
||||
/// This trait is automatically implemented on generated companion structs for entities
|
||||
/// that have fields marked with `#[georm(defaultable)]`. It provides a convenient way
|
||||
/// to create entities while allowing the database to provide default values for certain
|
||||
/// fields.
|
||||
///
|
||||
/// ## Generated Implementation
|
||||
///
|
||||
/// When you mark fields with `#[georm(defaultable)]`, Georm automatically generates:
|
||||
/// - A companion struct named `{EntityName}Default`
|
||||
/// - An implementation of this trait for the companion struct
|
||||
/// - Optimized SQL that omits defaultable fields when they are `None`
|
||||
///
|
||||
/// ## How It Works
|
||||
///
|
||||
/// The generated companion struct transforms defaultable fields into `Option<T>` types:
|
||||
/// - `Some(value)` - Use the provided value
|
||||
/// - `None` - Let the database provide the default value
|
||||
///
|
||||
/// Non-defaultable fields remain unchanged and are always required.
|
||||
///
|
||||
/// ## Database Behavior
|
||||
///
|
||||
/// The `create` method generates SQL that:
|
||||
/// - Only includes fields where `Some(value)` is provided
|
||||
/// - Omits fields that are `None`, allowing database defaults to apply
|
||||
/// - Uses `RETURNING *` to capture the final entity state with all defaults applied
|
||||
/// - Respects database triggers, sequences, and default value expressions
|
||||
///
|
||||
/// ## Usage Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// use georm::{Georm, Defaultable};
|
||||
///
|
||||
/// #[derive(Georm)]
|
||||
/// #[georm(table = "posts")]
|
||||
/// pub struct Post {
|
||||
/// #[georm(id, defaultable)]
|
||||
/// id: i32, // Auto-generated serial
|
||||
/// title: String, // Required field
|
||||
/// #[georm(defaultable)]
|
||||
/// published: bool, // Database default: false
|
||||
/// #[georm(defaultable)]
|
||||
/// created_at: chrono::DateTime<chrono::Utc>, // Database default: NOW()
|
||||
/// author_id: i32, // Required field
|
||||
/// }
|
||||
///
|
||||
/// // Generated automatically:
|
||||
/// // pub struct PostDefault {
|
||||
/// // pub id: Option<i32>,
|
||||
/// // pub title: String,
|
||||
/// // pub published: Option<bool>,
|
||||
/// // pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
/// // pub author_id: i32,
|
||||
/// // }
|
||||
/// //
|
||||
/// // impl Defaultable<i32, Post> for PostDefault { ... }
|
||||
///
|
||||
/// // Create with some defaults
|
||||
/// let post_default = PostDefault {
|
||||
/// id: None, // Let database auto-generate
|
||||
/// title: "My Blog Post".to_string(),
|
||||
/// published: None, // Use database default (false)
|
||||
/// created_at: None, // Use database default (NOW())
|
||||
/// author_id: 42,
|
||||
/// };
|
||||
///
|
||||
/// let created_post = post_default.create(&pool).await?;
|
||||
/// println!("Created post with ID: {}", created_post.id);
|
||||
///
|
||||
/// // Create with explicit values
|
||||
/// let post_default = PostDefault {
|
||||
/// id: None, // Still auto-generate ID
|
||||
/// title: "Published Post".to_string(),
|
||||
/// published: Some(true), // Override default
|
||||
/// created_at: Some(specific_time), // Override default
|
||||
/// author_id: 42,
|
||||
/// };
|
||||
/// ```
|
||||
///
|
||||
/// ## Type Parameters
|
||||
///
|
||||
/// - `Id` - The primary key type of the target entity (e.g., `i32`, `UserRoleId`)
|
||||
/// - `Entity` - The target entity type that will be created (e.g., `Post`, `User`)
|
||||
///
|
||||
/// ## Comparison with Regular Creation
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Using regular Georm::create - must provide all values
|
||||
/// let post = Post {
|
||||
/// id: 0, // Ignored for auto-increment, but required
|
||||
/// title: "My Post".to_string(),
|
||||
/// published: false, // Must specify even if it's the default
|
||||
/// created_at: chrono::Utc::now(), // Must calculate current time manually
|
||||
/// author_id: 42,
|
||||
/// };
|
||||
/// let created = post.create(&pool).await?;
|
||||
///
|
||||
/// // Using Defaultable::create - let database handle defaults
|
||||
/// let post_default = PostDefault {
|
||||
/// id: None, // Clearer intent for auto-generation
|
||||
/// title: "My Post".to_string(),
|
||||
/// published: None, // Let database default apply
|
||||
/// created_at: None, // Let database calculate NOW()
|
||||
/// author_id: 42,
|
||||
/// };
|
||||
/// let created = post_default.create(&pool).await?;
|
||||
/// ```
|
||||
///
|
||||
/// ## Field Visibility
|
||||
///
|
||||
/// The generated companion struct preserves the field visibility of the original entity:
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[derive(Georm)]
|
||||
/// #[georm(table = "posts")]
|
||||
/// pub struct Post {
|
||||
/// #[georm(id, defaultable)]
|
||||
/// pub id: i32,
|
||||
/// pub title: String,
|
||||
/// #[georm(defaultable)]
|
||||
/// pub(crate) internal_status: String, // Crate-private field
|
||||
/// #[georm(defaultable)]
|
||||
/// private_field: String, // Private field
|
||||
/// }
|
||||
///
|
||||
/// // Generated with preserved visibility:
|
||||
/// // pub struct PostDefault {
|
||||
/// // pub id: Option<i32>,
|
||||
/// // pub title: String,
|
||||
/// // pub(crate) internal_status: Option<String>, // Preserved
|
||||
/// // private_field: Option<String>, // Preserved
|
||||
/// // }
|
||||
/// ```
|
||||
///
|
||||
/// ## Limitations and Rules
|
||||
///
|
||||
/// - **Option fields cannot be defaultable**: Fields that are already `Option<T>` cannot
|
||||
/// be marked with `#[georm(defaultable)]` to prevent `Option<Option<T>>` types
|
||||
/// - **Compile-time validation**: Attempts to mark `Option<T>` fields as defaultable
|
||||
/// result in compile-time errors
|
||||
/// - **Requires at least one defaultable field**: The companion struct is only generated
|
||||
/// if at least one field is marked as defaultable
|
||||
/// - **No partial updates**: This trait only supports creating new entities, not updating
|
||||
/// existing ones with defaults
|
||||
///
|
||||
/// ## Error Handling
|
||||
///
|
||||
/// The `create` method can fail for the same reasons as regular entity creation:
|
||||
/// - Database connection issues
|
||||
/// - Constraint violations (unique, foreign key, NOT NULL for non-defaultable fields)
|
||||
/// - Permission problems
|
||||
/// - Table or column doesn't exist
|
||||
///
|
||||
/// ## Performance Characteristics
|
||||
///
|
||||
/// - **Efficient SQL**: Only includes necessary fields in the INSERT statement
|
||||
/// - **Single round-trip**: Uses `RETURNING *` to get the final entity state
|
||||
/// - **No overhead**: Defaultable logic is resolved at compile time
|
||||
/// - **Database-optimized**: Leverages database defaults rather than application logic
|
||||
pub trait Defaultable<Id, Entity> {
|
||||
/// Creates an entity in the database.
|
||||
/// Create a new entity in the database using database defaults for unspecified fields.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error the database may have encountered
|
||||
/// This method constructs and executes an `INSERT INTO table_name (...) VALUES (...) RETURNING *`
|
||||
/// query that only includes fields where `Some(value)` is provided. Fields that are `None`
|
||||
/// are omitted from the query, allowing the database to apply default values, auto-increment
|
||||
/// sequences, or trigger-generated values.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `pool` - Database connection pool
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(Entity)` - The newly created entity with all database-generated values populated
|
||||
/// - `Err(sqlx::Error)` - Database constraint violations or connection errors
|
||||
///
|
||||
/// # Database Behavior
|
||||
/// - **Selective field inclusion**: Only includes fields with `Some(value)` in the INSERT
|
||||
/// - **Default value application**: Database defaults apply to omitted fields
|
||||
/// - **RETURNING clause**: Captures the complete entity state after insertion
|
||||
/// - **Trigger execution**: Database triggers run and their effects are captured
|
||||
/// - **Sequence generation**: Auto-increment values are generated and returned
|
||||
///
|
||||
/// # SQL Generation
|
||||
///
|
||||
/// The generated SQL dynamically includes only the necessary fields:
|
||||
///
|
||||
/// ```sql
|
||||
/// -- If id=None, published=None, created_at=None:
|
||||
/// INSERT INTO posts (title, author_id) VALUES ($1, $2) RETURNING *;
|
||||
///
|
||||
/// -- If id=None, published=Some(true), created_at=None:
|
||||
/// INSERT INTO posts (title, published, author_id) VALUES ($1, $2, $3) RETURNING *;
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Minimal creation - let database handle all defaults
|
||||
/// let post_default = PostDefault {
|
||||
/// id: None, // Auto-generated
|
||||
/// title: "Hello World".to_string(),
|
||||
/// published: None, // Database default
|
||||
/// created_at: None, // Database default (NOW())
|
||||
/// author_id: 1,
|
||||
/// };
|
||||
/// let post = post_default.create(&pool).await?;
|
||||
///
|
||||
/// // Mixed creation - some explicit values, some defaults
|
||||
/// let post_default = PostDefault {
|
||||
/// id: None, // Auto-generated
|
||||
/// title: "Published Post".to_string(),
|
||||
/// published: Some(true), // Override default
|
||||
/// created_at: None, // Still use database default
|
||||
/// author_id: 1,
|
||||
/// };
|
||||
/// let post = post_default.create(&pool).await?;
|
||||
///
|
||||
/// // Full control - specify all defaultable values
|
||||
/// let specific_time = chrono::Utc::now() - chrono::Duration::hours(1);
|
||||
/// let post_default = PostDefault {
|
||||
/// id: Some(100), // Explicit ID (if not auto-increment)
|
||||
/// title: "Backdated Post".to_string(),
|
||||
/// published: Some(false), // Explicit value
|
||||
/// created_at: Some(specific_time), // Explicit timestamp
|
||||
/// author_id: 1,
|
||||
/// };
|
||||
/// let post = post_default.create(&pool).await?;
|
||||
/// ```
|
||||
///
|
||||
/// # Error Conditions
|
||||
///
|
||||
/// Returns `sqlx::Error` for:
|
||||
/// - **Unique constraint violations**: Duplicate values for unique fields
|
||||
/// - **Foreign key violations**: Invalid references to other tables
|
||||
/// - **NOT NULL violations**: Missing values for required non-defaultable fields
|
||||
/// - **Check constraint violations**: Values that don't meet database constraints
|
||||
/// - **Database connection issues**: Network or connection pool problems
|
||||
/// - **Permission problems**: Insufficient privileges for the operation
|
||||
/// - **Table/column errors**: Missing tables or columns (usually caught at compile time)
|
||||
///
|
||||
/// # Performance Notes
|
||||
///
|
||||
/// - **Optimal field selection**: Only transmits necessary data to the database
|
||||
/// - **Single database round-trip**: INSERT and retrieval in one operation
|
||||
/// - **Compile-time optimization**: Field inclusion logic resolved at compile time
|
||||
/// - **Database-native defaults**: Leverages database performance for default value generation
|
||||
///
|
||||
/// # Comparison with Standard Creation
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Standard Georm::create - all fields required
|
||||
/// let post = Post {
|
||||
/// id: 0, // Placeholder for auto-increment
|
||||
/// title: "My Post".to_string(),
|
||||
/// published: false, // Must specify, even if it's the default
|
||||
/// created_at: chrono::Utc::now(), // Must calculate manually
|
||||
/// author_id: 1,
|
||||
/// };
|
||||
/// let created = post.create(&pool).await?;
|
||||
///
|
||||
/// // Defaultable::create - only specify what you need
|
||||
/// let post_default = PostDefault {
|
||||
/// id: None, // Clear intent for auto-generation
|
||||
/// title: "My Post".to_string(),
|
||||
/// published: None, // Let database decide
|
||||
/// created_at: None, // Let database calculate
|
||||
/// author_id: 1,
|
||||
/// };
|
||||
/// let created = post_default.create(&pool).await?;
|
||||
/// ```
|
||||
fn create(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Entity>> + Send;
|
||||
) -> impl std::future::Future<Output = sqlx::Result<Entity>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
323
src/georm.rs
323
src/georm.rs
@ -1,18 +1,157 @@
|
||||
/// Core database operations trait for Georm entities.
|
||||
///
|
||||
/// This trait is automatically implemented by the `#[derive(Georm)]` macro and provides
|
||||
/// all essential CRUD operations for database entities. The trait is generic over the
|
||||
/// primary key type `Id`, which can be a simple type (e.g., `i32`) or a generated
|
||||
/// composite key struct (e.g., `UserRoleId`).
|
||||
///
|
||||
/// ## Generated Implementation
|
||||
///
|
||||
/// When you derive `Georm` on a struct, this trait is automatically implemented with
|
||||
/// PostgreSQL-optimized queries that use:
|
||||
/// - **Prepared statements** for security and performance
|
||||
/// - **RETURNING clause** to capture database-generated values
|
||||
/// - **ON CONFLICT** for efficient upsert operations
|
||||
/// - **Compile-time verification** via SQLx macros
|
||||
///
|
||||
/// ## Method Categories
|
||||
///
|
||||
/// ### Static Methods (Query Operations)
|
||||
/// - [`find_all`] - Retrieve all entities from the table
|
||||
/// - [`find`] - Retrieve a single entity by primary key
|
||||
/// - [`delete_by_id`] - Delete an entity by primary key
|
||||
///
|
||||
/// ### Instance Methods (Mutation Operations)
|
||||
/// - [`create`] - Insert a new entity into the database
|
||||
/// - [`update`] - Update an existing entity in the database
|
||||
/// - [`create_or_update`] - Upsert (insert or update) an entity
|
||||
/// - [`delete`] - Delete this entity from the database
|
||||
/// - [`get_id`] - Get the primary key of this entity
|
||||
///
|
||||
/// ## Usage Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// use georm::Georm;
|
||||
///
|
||||
/// #[derive(Georm)]
|
||||
/// #[georm(table = "users")]
|
||||
/// struct User {
|
||||
/// #[georm(id)]
|
||||
/// id: i32,
|
||||
/// username: String,
|
||||
/// email: String,
|
||||
/// }
|
||||
///
|
||||
/// // Static methods
|
||||
/// let all_users = User::find_all(&pool).await?;
|
||||
/// let user = User::find(&pool, &1).await?;
|
||||
/// let deleted_count = User::delete_by_id(&pool, &1).await?;
|
||||
///
|
||||
/// // Instance methods
|
||||
/// let new_user = User { id: 0, username: "alice".into(), email: "alice@example.com".into() };
|
||||
/// let created = new_user.create(&pool).await?;
|
||||
/// let updated = created.update(&pool).await?;
|
||||
/// let id = updated.get_id();
|
||||
/// let deleted_count = updated.delete(&pool).await?;
|
||||
/// ```
|
||||
///
|
||||
/// ## Composite Key Support
|
||||
///
|
||||
/// For entities with composite primary keys, the `Id` type parameter becomes a generated
|
||||
/// struct following the pattern `{EntityName}Id`:
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[derive(Georm)]
|
||||
/// #[georm(table = "user_roles")]
|
||||
/// struct UserRole {
|
||||
/// #[georm(id)]
|
||||
/// user_id: i32,
|
||||
/// #[georm(id)]
|
||||
/// role_id: i32,
|
||||
/// assigned_at: chrono::DateTime<chrono::Utc>,
|
||||
/// }
|
||||
///
|
||||
/// // Generated: pub struct UserRoleId { pub user_id: i32, pub role_id: i32 }
|
||||
/// // Trait: impl Georm<UserRoleId> for UserRole
|
||||
///
|
||||
/// let id = UserRoleId { user_id: 1, role_id: 2 };
|
||||
/// let user_role = UserRole::find(&pool, &id).await?;
|
||||
/// ```
|
||||
///
|
||||
/// ## Error Handling
|
||||
///
|
||||
/// All methods return `sqlx::Result<T>` and may fail due to:
|
||||
/// - Database connection issues
|
||||
/// - Constraint violations (unique, foreign key, etc.)
|
||||
/// - Invalid queries (though most are caught at compile time)
|
||||
/// - Missing records (for operations expecting existing data)
|
||||
///
|
||||
/// [`find_all`]: Georm::find_all
|
||||
/// [`find`]: Georm::find
|
||||
/// [`create`]: Georm::create
|
||||
/// [`update`]: Georm::update
|
||||
/// [`create_or_update`]: Georm::create_or_update
|
||||
/// [`delete`]: Georm::delete
|
||||
/// [`delete_by_id`]: Georm::delete_by_id
|
||||
/// [`get_id`]: Georm::get_id
|
||||
pub trait Georm<Id> {
|
||||
/// Find all the entities in the database.
|
||||
/// Retrieve all entities from the database table.
|
||||
///
|
||||
/// This method executes a `SELECT * FROM table_name` query and returns all records
|
||||
/// as a vector of entities. The results are not paginated or filtered.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(Vec<Self>)` - All entities in the table (may be empty)
|
||||
/// - `Err(sqlx::Error)` - Database connection or query execution errors
|
||||
///
|
||||
/// # Performance Notes
|
||||
/// - Returns all records in memory - consider pagination for large tables
|
||||
/// - Uses prepared statements for optimal performance
|
||||
/// - No built-in ordering - results may vary between calls
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let all_users = User::find_all(&pool).await?;
|
||||
/// println!("Found {} users", all_users.len());
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
/// Returns `sqlx::Error` for database connection issues, permission problems,
|
||||
/// or if the table doesn't exist.
|
||||
fn find_all(
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl ::std::future::Future<Output = ::sqlx::Result<Vec<Self>>> + Send
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Find the entiy in the database based on its identifier.
|
||||
/// Find a single entity by its primary key.
|
||||
///
|
||||
/// This method executes a `SELECT * FROM table_name WHERE primary_key = $1` query
|
||||
/// (or equivalent for composite keys) and returns the matching entity if found.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `pool` - Database connection pool
|
||||
/// - `id` - Primary key value (simple type or composite key struct)
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(Some(Self))` - Entity found and returned
|
||||
/// - `Ok(None)` - No entity with the given ID exists
|
||||
/// - `Err(sqlx::Error)` - Database connection or query execution errors
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// // Simple primary key
|
||||
/// let user = User::find(&pool, &1).await?;
|
||||
///
|
||||
/// // Composite primary key
|
||||
/// let id = UserRoleId { user_id: 1, role_id: 2 };
|
||||
/// let user_role = UserRole::find(&pool, &id).await?;
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
/// Returns `sqlx::Error` for database connection issues, type conversion errors,
|
||||
/// or query execution problems. Note that not finding a record is not an error
|
||||
/// - it returns `Ok(None)`.
|
||||
fn find(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &Id,
|
||||
@ -20,10 +159,38 @@ pub trait Georm<Id> {
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Create the entity in the database.
|
||||
/// Insert this entity as a new record in the database.
|
||||
///
|
||||
/// This method executes an `INSERT INTO table_name (...) VALUES (...) RETURNING *`
|
||||
/// query and returns the newly created entity with any database-generated values
|
||||
/// (such as auto-increment IDs, default timestamps, etc.).
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `pool` - Database connection pool
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(Self)` - The entity as it exists in the database after insertion
|
||||
/// - `Err(sqlx::Error)` - Database constraint violations or connection errors
|
||||
///
|
||||
/// # Database Behavior
|
||||
/// - Uses `RETURNING *` to capture database-generated values
|
||||
/// - Respects database defaults for fields marked `#[georm(defaultable)]`
|
||||
/// - Triggers and database-side modifications are reflected in the returned entity
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let new_user = User { id: 0, username: "alice".into(), email: "alice@example.com".into() };
|
||||
/// let created_user = new_user.create(&pool).await?;
|
||||
/// println!("Created user with ID: {}", created_user.id);
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
/// Returns `sqlx::Error` for:
|
||||
/// - Unique constraint violations
|
||||
/// - Foreign key constraint violations
|
||||
/// - NOT NULL constraint violations
|
||||
/// - Database connection issues
|
||||
/// - Permission problems
|
||||
fn create(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
@ -31,10 +198,37 @@ pub trait Georm<Id> {
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Update an entity with a matching identifier in the database.
|
||||
/// Update an existing entity in the database.
|
||||
///
|
||||
/// This method executes an `UPDATE table_name SET ... WHERE primary_key = ... RETURNING *`
|
||||
/// query using the entity's current primary key to locate the record to update.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `pool` - Database connection pool
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(Self)` - The entity as it exists in the database after the update
|
||||
/// - `Err(sqlx::Error)` - Database errors or if no matching record exists
|
||||
///
|
||||
/// # Database Behavior
|
||||
/// - Uses `RETURNING *` to capture any database-side changes
|
||||
/// - Updates all fields, not just changed ones
|
||||
/// - Triggers and database-side modifications are reflected in the returned entity
|
||||
/// - Fails if no record with the current primary key exists
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let mut user = User::find(&pool, &1).await?.unwrap();
|
||||
/// user.email = "newemail@example.com".into();
|
||||
/// let updated_user = user.update(&pool).await?;
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
/// Returns `sqlx::Error` for:
|
||||
/// - No matching record found (record was deleted by another process)
|
||||
/// - Constraint violations (unique, foreign key, etc.)
|
||||
/// - Database connection issues
|
||||
/// - Permission problems
|
||||
fn update(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
@ -42,11 +236,37 @@ pub trait Georm<Id> {
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Update an entity with a matching identifier in the database if
|
||||
/// it exists, create it otherwise.
|
||||
/// Insert or update this entity using PostgreSQL's upsert functionality.
|
||||
///
|
||||
/// This method executes an `INSERT ... ON CONFLICT (...) DO UPDATE SET ... RETURNING *`
|
||||
/// query that atomically inserts the entity if it doesn't exist, or updates it if
|
||||
/// a record with the same primary key already exists.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `pool` - Database connection pool
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(Self)` - The final entity state in the database (inserted or updated)
|
||||
/// - `Err(sqlx::Error)` - Database connection or constraint violation errors
|
||||
///
|
||||
/// # Database Behavior
|
||||
/// - Uses PostgreSQL's `ON CONFLICT` for true atomic upsert
|
||||
/// - More efficient than separate find-then-create-or-update logic
|
||||
/// - Uses `RETURNING *` to capture the final state
|
||||
/// - Conflict resolution is based on the primary key constraint
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let user = User { id: 1, username: "alice".into(), email: "alice@example.com".into() };
|
||||
/// let final_user = user.create_or_update(&pool).await?;
|
||||
/// // Will insert if ID 1 doesn't exist, update if it does
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
/// Returns `sqlx::Error` for:
|
||||
/// - Non-primary-key constraint violations
|
||||
/// - Database connection issues
|
||||
/// - Permission problems
|
||||
fn create_or_update(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
@ -54,30 +274,97 @@ pub trait Georm<Id> {
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Delete the entity from the database if it exists.
|
||||
/// Delete this entity from the database.
|
||||
///
|
||||
/// This method executes a `DELETE FROM table_name WHERE primary_key = ...` query
|
||||
/// using this entity's primary key to identify the record to delete.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `pool` - Database connection pool
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the amount of rows affected by the deletion.
|
||||
/// - `Ok(u64)` - Number of rows affected (0 if entity didn't exist, 1 if deleted)
|
||||
/// - `Err(sqlx::Error)` - Database connection or constraint violation errors
|
||||
///
|
||||
/// # Database Behavior
|
||||
/// - Uses the entity's current primary key for deletion
|
||||
/// - Returns 0 if no matching record exists (not an error)
|
||||
/// - May fail due to foreign key constraints if other records reference this entity
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let user = User::find(&pool, &1).await?.unwrap();
|
||||
/// let deleted_count = user.delete(&pool).await?;
|
||||
/// assert_eq!(deleted_count, 1);
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
/// Returns `sqlx::Error` for:
|
||||
/// - Foreign key constraint violations (referenced by other tables)
|
||||
/// - Database connection issues
|
||||
/// - Permission problems
|
||||
fn delete(
|
||||
&self,
|
||||
pool: &sqlx::PgPool,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
||||
|
||||
/// Delete any entity with the identifier `id`.
|
||||
/// Delete an entity by its primary key without needing an entity instance.
|
||||
///
|
||||
/// This method executes a `DELETE FROM table_name WHERE primary_key = ...` query
|
||||
/// using the provided ID to identify the record to delete.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `pool` - Database connection pool
|
||||
/// - `id` - Primary key value (simple type or composite key struct)
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the amount of rows affected by the deletion.
|
||||
/// - `Ok(u64)` - Number of rows affected (0 if entity didn't exist, 1 if deleted)
|
||||
/// - `Err(sqlx::Error)` - Database connection or constraint violation errors
|
||||
///
|
||||
/// # Database Behavior
|
||||
/// - More efficient than `find().delete()` when you only have the ID
|
||||
/// - Returns 0 if no matching record exists (not an error)
|
||||
/// - May fail due to foreign key constraints if other records reference this entity
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// // Simple primary key
|
||||
/// let deleted_count = User::delete_by_id(&pool, &1).await?;
|
||||
///
|
||||
/// // Composite primary key
|
||||
/// let id = UserRoleId { user_id: 1, role_id: 2 };
|
||||
/// let deleted_count = UserRole::delete_by_id(&pool, &id).await?;
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns any error Postgres may have encountered
|
||||
/// Returns `sqlx::Error` for:
|
||||
/// - Foreign key constraint violations (referenced by other tables)
|
||||
/// - Database connection issues
|
||||
/// - Permission problems
|
||||
fn delete_by_id(
|
||||
pool: &sqlx::PgPool,
|
||||
id: &Id,
|
||||
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send;
|
||||
|
||||
/// Returns the identifier of the entity.
|
||||
/// Get the primary key of this entity.
|
||||
///
|
||||
/// For entities with simple primary keys, this returns the ID value directly.
|
||||
/// For entities with composite primary keys, this returns an owned instance of
|
||||
/// the generated `{EntityName}Id` struct.
|
||||
///
|
||||
/// # Returns
|
||||
/// - Simple keys: The primary key value (e.g., `i32`, `String`)
|
||||
/// - Composite keys: Generated ID struct (e.g., `UserRoleId`)
|
||||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// // Simple primary key
|
||||
/// let user = User { id: 42, username: "alice".into(), email: "alice@example.com".into() };
|
||||
/// let id = user.get_id(); // Returns 42
|
||||
///
|
||||
/// // Composite primary key
|
||||
/// let user_role = UserRole { user_id: 1, role_id: 2, assigned_at: now };
|
||||
/// let id = user_role.get_id(); // Returns UserRoleId { user_id: 1, role_id: 2 }
|
||||
/// ```
|
||||
fn get_id(&self) -> Id;
|
||||
}
|
||||
|
753
src/lib.rs
753
src/lib.rs
@ -1,281 +1,135 @@
|
||||
//! # Georm
|
||||
//!
|
||||
//! ## Introduction
|
||||
//! A simple, type-safe PostgreSQL ORM built on SQLx with zero runtime overhead.
|
||||
//!
|
||||
//! Georm is a simple, opinionated SQLx ORM for PostgreSQL.
|
||||
//!
|
||||
//! To automatically implement the `Georm` trait, you need at least:
|
||||
//! - to derive the `Georm` and `sqlx::FromRow` traits
|
||||
//! - use the `georm` proc-macro to indicate the table in which your entity
|
||||
//! lives
|
||||
//! - use the `georm` proc-macro again to indicate which field of your struct is
|
||||
//! the identifier of your entity.
|
||||
//!
|
||||
//! ## Simple usage
|
||||
//! Here is a minimal use of Georm with a struct:
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! use georm::Georm;
|
||||
//!
|
||||
//! // Note: No need to derive FromRow - Georm generates it automatically
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "users")]
|
||||
//! pub struct User {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! username: String,
|
||||
//! hashed_password: String,
|
||||
//! email: String,
|
||||
//! }
|
||||
//!
|
||||
//! // Use generated methods
|
||||
//! let user = User::find(&pool, &1).await?; // Static method
|
||||
//! let all_users = User::find_all(&pool).await?; // Static method
|
||||
//! user.update(&pool).await?; // Instance method
|
||||
//! ```
|
||||
//!
|
||||
//! The `User` type will now have access to all the functions declared in the
|
||||
//! `Georm` trait.
|
||||
//! ## Core CRUD Operations
|
||||
//!
|
||||
//! ## One-to-one relationships
|
||||
//! ### Static Methods (called on the struct type)
|
||||
//! - `Entity::find(pool, &id)` - Find by primary key, returns `Option<Entity>`
|
||||
//! - `Entity::find_all(pool)` - Get all records, returns `Vec<Entity>`
|
||||
//! - `Entity::delete_by_id(pool, &id)` - Delete by ID, returns affected row count
|
||||
//!
|
||||
//! You can then create relationships between different entities. For instance,
|
||||
//! you can use an identifier of another entity as a link to that other entity.
|
||||
//! ### Instance Methods (called on entity objects)
|
||||
//! - `entity.create(pool)` - Insert new record, returns created entity with database-generated values
|
||||
//! - `entity.update(pool)` - Update existing record, returns updated entity with fresh database state
|
||||
//! - `entity.create_or_update(pool)` - True PostgreSQL upsert using `ON CONFLICT`, returns final entity
|
||||
//! - `entity.delete(pool)` - Delete this record, returns affected row count
|
||||
//! - `entity.get_id()` - Get reference to the entity's ID (`&Id` for simple keys, owned for composite)
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(table = "profiles")]
|
||||
//! pub struct Profile {
|
||||
//! // Static methods
|
||||
//! let user = User::find(&pool, &1).await?.unwrap();
|
||||
//! let all_users = User::find_all(&pool).await?;
|
||||
//! let deleted_count = User::delete_by_id(&pool, &1).await?;
|
||||
//!
|
||||
//! // Instance methods
|
||||
//! let new_user = User { id: 0, username: "alice".to_string(), email: "alice@example.com".to_string() };
|
||||
//! let created = new_user.create(&pool).await?; // Returns entity with actual generated ID
|
||||
//! let updated = created.update(&pool).await?; // Returns entity with fresh database state
|
||||
//! let deleted_count = updated.delete(&pool).await?;
|
||||
//! ```
|
||||
//!
|
||||
//! ### PostgreSQL Optimizations
|
||||
//!
|
||||
//! Georm leverages PostgreSQL-specific features for performance and reliability:
|
||||
//!
|
||||
//! - **RETURNING clause**: All `INSERT` and `UPDATE` operations use `RETURNING *` to capture database-generated values (sequences, defaults, triggers)
|
||||
//! - **True upserts**: `create_or_update()` uses `INSERT ... ON CONFLICT ... DO UPDATE` for atomic upsert operations
|
||||
//! - **Prepared statements**: All queries use parameter binding for security and performance
|
||||
//! - **Compile-time verification**: SQLx macros verify all generated SQL against your database schema at compile time
|
||||
//!
|
||||
//! ## Primary Keys and Identifiers
|
||||
//!
|
||||
//! ### Simple Primary Keys
|
||||
//!
|
||||
//! Primary key fields can have any name (not just "id"):
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "books")]
|
||||
//! pub struct Book {
|
||||
//! #[georm(id)]
|
||||
//! ident: i32, // Custom field name for primary key
|
||||
//! title: String,
|
||||
//! }
|
||||
//!
|
||||
//! // Works the same way
|
||||
//! let book = Book::find(&pool, &1).await?;
|
||||
//! ```
|
||||
//!
|
||||
//! ### Composite Primary Keys
|
||||
//!
|
||||
//! Mark multiple fields with `#[georm(id)]` for composite keys:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "user_roles")]
|
||||
//! pub struct UserRole {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! #[georm(
|
||||
//! relation = {
|
||||
//! entity = User,
|
||||
//! name = "user",
|
||||
//! table = "users",
|
||||
//! remote_id = "id",
|
||||
//! nullable = false
|
||||
//! })
|
||||
//! ]
|
||||
//! user_id: i32,
|
||||
//! display_name: String,
|
||||
//! #[georm(id)]
|
||||
//! role_id: i32,
|
||||
//! assigned_at: chrono::DateTime<chrono::Utc>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This will give access to the `Profile::get_user(&self, pool: &sqlx::PgPool)
|
||||
//! -> User` method.
|
||||
//!
|
||||
//! Here is an explanation of what these different values mean:
|
||||
//!
|
||||
//! | Value Name | Explanation | Default value |
|
||||
//! |------------|------------------------------------------------------------------------------------------|---------------|
|
||||
//! | entity | Rust type of the entity found in the database | N/A |
|
||||
//! | name | Name of the remote entity within the local entity; generates a method named `get_{name}` | N/A |
|
||||
//! | table | Database table where the entity is stored | N/A |
|
||||
//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` |
|
||||
//! | nullable | Whether the relationship can be broken | `false` |
|
||||
//!
|
||||
//! Note that in this instance, the `remote_id` and `nullable` values can be
|
||||
//! omitted as this is their default value. This below is a strict equivalent:
|
||||
//! This automatically generates a composite ID struct following the `{EntityName}Id` pattern:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(table = "profiles")]
|
||||
//! pub struct Profile {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! #[georm(relation = { entity = User, table = "users", name = "user" })]
|
||||
//! user_id: i32,
|
||||
//! display_name: String,
|
||||
//! // Generated automatically by the macro
|
||||
//! pub struct UserRoleId {
|
||||
//! pub user_id: i32,
|
||||
//! pub role_id: i32,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! But what if I have a one-to-one relationship with another entity and
|
||||
//! my current entity holds no data to reference that other identity? No
|
||||
//! worries, there is another way to declare such relationships.
|
||||
//! Usage with composite keys:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[georm(
|
||||
//! one_to_one = [{
|
||||
//! name = "profile",
|
||||
//! remote_id = "user_id",
|
||||
//! table = "profiles",
|
||||
//! entity = User
|
||||
//! }]
|
||||
//! )]
|
||||
//! struct User {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! username: String,
|
||||
//! hashed_password: String,
|
||||
//! }
|
||||
//! // Static methods work with generated ID structs
|
||||
//! let id = UserRoleId { user_id: 1, role_id: 2 };
|
||||
//! let user_role = UserRole::find(&pool, &id).await?;
|
||||
//! UserRole::delete_by_id(&pool, &id).await?;
|
||||
//!
|
||||
//! // Instance methods work the same way
|
||||
//! let role = UserRole { user_id: 1, role_id: 2, assigned_at: chrono::Utc::now() };
|
||||
//! let created = role.create(&pool).await?;
|
||||
//! let id = created.get_id(); // Returns owned UserRoleId for composite keys
|
||||
//! ```
|
||||
//!
|
||||
//! We now have access to the method `User::get_profile(&self, &pool:
|
||||
//! sqlx::PgPool) -> Option<User>`.
|
||||
//! ### Composite Key Limitations
|
||||
//!
|
||||
//! Here is an explanation of the values of `one_to_many`:
|
||||
//!
|
||||
//! | Value Name | Explanaion | Default Value |
|
||||
//! |------------|------------------------------------------------------------------------------------------|---------------|
|
||||
//! | entity | Rust type of the entity found in the database | N/A |
|
||||
//! | name | Name of the remote entity within the local entity; generates a method named `get_{name}` | N/A |
|
||||
//! | table | Database table where the entity is stored | N/A |
|
||||
//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` |
|
||||
//!
|
||||
//! ## One-to-many relationships
|
||||
//!
|
||||
//! Sometimes, our entity is the one being referenced to by multiple entities,
|
||||
//! but we have no internal reference to these remote entities in our local
|
||||
//! entity. Fortunately, we have a way to indicate to Georm how to find these.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(table = "posts")]
|
||||
//! struct Post {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! #[georm(relation = { entity = User, table = "users", name = "user" })]
|
||||
//! author_id: i32,
|
||||
//! content: String
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(
|
||||
//! table = "users",
|
||||
//! one_to_many = [{
|
||||
//! entity = Post,
|
||||
//! name = "posts",
|
||||
//! table = "posts",
|
||||
//! remote_id = "author_id"
|
||||
//! }]
|
||||
//! )]
|
||||
//! struct User {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! username: String,
|
||||
//! hashed_password: String
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! As we’ve seen earlier, the struct `Post` has access to the method
|
||||
//! `Post::get_user(&self, pool: &sqlx::PgPool) -> User` thanks to the
|
||||
//! proc-macro used on `author_id`. However, `User` now has also access to
|
||||
//! `User::get_posts(&self, pool: &sqlx::PgPool) -> Vec<Post>`. And as you can
|
||||
//! see, `one_to_many` is an array, meaning you can define several one-to-many
|
||||
//! relationships for `User`.
|
||||
//!
|
||||
//! Here is an explanation of the values of `one_to_many`:
|
||||
//!
|
||||
//! | Value Name | Explanaion | Default Value |
|
||||
//! |------------|------------------------------------------------------------------------------------------|---------------|
|
||||
//! | entity | Rust type of the entity found in the database | N/A |
|
||||
//! | name | Name of the remote entity within the local entity; generates a method named `get_{name}` | N/A |
|
||||
//! | table | Database table where the entity is stored | N/A |
|
||||
//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` |
|
||||
//!
|
||||
//! As with one-to-one relationships, `remote_id` is optional. The following
|
||||
//! `User` struct is strictly equivalent.
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(
|
||||
//! table = "users",
|
||||
//! one_to_many = [{ entity = Post, name = "posts", table = "posts" }]
|
||||
//! )]
|
||||
//! struct User {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! username: String,
|
||||
//! hashed_password: String
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Many-to-many relationships
|
||||
//!
|
||||
//! Many-to-many relationships between entities A and entities B with Georm rely
|
||||
//! on a third table which refers to both. For instance, the following SQL code
|
||||
//! describes a many-to-many relationship between books and book genre.
|
||||
//!
|
||||
//! ```sql
|
||||
//! CREATE TABLE books (
|
||||
//! id SERIAL PRIMARY KEY,
|
||||
//! title VARCHAR(100) NOT NULL
|
||||
//! );
|
||||
//!
|
||||
//! CREATE TABLE genres (
|
||||
//! id SERIAL PRIMARY KEY,
|
||||
//! name VARCHAR(100) NOT NULL
|
||||
//! );
|
||||
//!
|
||||
//! CREATE TABLE books_genres (
|
||||
//! book_id INT NOT NULL,
|
||||
//! genre_id INT NOT NULL,
|
||||
//! PRIMARY KEY (book_id, genre_id),
|
||||
//! FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
|
||||
//! FOREIGN KEY (genre_id) REFERENCES genres(id) ON DELETE CASCADE
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! The table `books_genres` is the one defining the many-to-many relationship
|
||||
//! between the table `books` and the table `genres`. With Georm, this gives us
|
||||
//! the following code:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(
|
||||
//! table = "books",
|
||||
//! many_to_many = [{
|
||||
//! name = "genres",
|
||||
//! entity = Genre,
|
||||
//! table = "genres",
|
||||
//! remote_id = "id",
|
||||
//! link = { table = "books_genres", from = "book_id", to = "genre_id" }
|
||||
//! }]
|
||||
//! )]
|
||||
//! struct Book {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! title: String
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(
|
||||
//! table = "genres",
|
||||
//! many_to_many = [{
|
||||
//! entity = Book,
|
||||
//! name = "books",
|
||||
//! table = "books",
|
||||
//! remote_id = "id",
|
||||
//! link = { table = "books_genres", from = "genre_id", to = "book_id" }
|
||||
//! }]
|
||||
//! )]
|
||||
//! struct Genre {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! name: String
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This generates two methods:
|
||||
//! - `Book::get_genres(&self, pool: &sqlx::PgPool) -> Vec<Genre>`
|
||||
//! - `Genre::get_books(&self, pool: &sqlx::PgPool) -> Vec<Book>`
|
||||
//!
|
||||
//! As you can see, `many_to_many` is also an array, meaning we can define
|
||||
//! several many-to-many relationships for the same struct.
|
||||
//!
|
||||
//! Here is an explanation of the values behind `many_to_many`:
|
||||
//!
|
||||
//! | Value Name | Explanation | Default value |
|
||||
//! |------------|------------------------------------------------------------------------------------------|---------------|
|
||||
//! | entity | Rust type of the entity found in the database | N/A |
|
||||
//! | name | Name of the remote entity within the local entity; generates a method named `get_{name}` | N/A |
|
||||
//! | table | Database table where the entity is stored | N/A |
|
||||
//! | remote_id | Name of the column serving as the identifier of the entity | `"id"` |
|
||||
//! | link.table | Name of the many-to-many relationship table | N/A |
|
||||
//! | link.from | Column of the linking table referring to this entity | N/A |
|
||||
//! | link.to | Column of the linking table referring to the remote entity | N/A |
|
||||
//! - **Relationships not supported**: Entities with composite primary keys cannot
|
||||
//! yet define relationships (one-to-one, one-to-many, many-to-many)
|
||||
//! - **ID struct naming**: Generated ID struct follows pattern `{EntityName}Id` (not customizable)
|
||||
//!
|
||||
//! ## Defaultable Fields
|
||||
//!
|
||||
//! Georm supports defaultable fields for entities where some fields have database
|
||||
//! defaults or are auto-generated (like serial IDs). When you mark fields as
|
||||
//! `defaultable`, Georm generates a companion struct that makes these fields
|
||||
//! optional during entity creation.
|
||||
//! Use `#[georm(defaultable)]` for fields with database defaults or auto-generated values:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "posts")]
|
||||
//! pub struct Post {
|
||||
//! #[georm(id, defaultable)]
|
||||
@ -284,12 +138,14 @@
|
||||
//! #[georm(defaultable)]
|
||||
//! published: bool, // Has database default
|
||||
//! #[georm(defaultable)]
|
||||
//! created_at: chrono::DateTime<chrono::Utc>, // Has database default
|
||||
//! created_at: chrono::DateTime<chrono::Utc>, // DEFAULT NOW()
|
||||
//! #[georm(defaultable)]
|
||||
//! pub(crate) internal_note: String, // Field visibility preserved
|
||||
//! author_id: i32, // Required field
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This generates a `PostDefault` struct where defaultable fields become `Option<T>`:
|
||||
//! This generates a companion `PostDefault` struct where defaultable fields become `Option<T>`:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // Generated automatically by the macro
|
||||
@ -298,6 +154,7 @@
|
||||
//! pub title: String, // Required field stays the same
|
||||
//! pub published: Option<bool>, // Can be None to use database default
|
||||
//! pub created_at: Option<chrono::DateTime<chrono::Utc>>, // Can be None
|
||||
//! pub(crate) internal_note: Option<String>, // Visibility preserved
|
||||
//! pub author_id: i32, // Required field stays the same
|
||||
//! }
|
||||
//!
|
||||
@ -317,90 +174,412 @@
|
||||
//! title: "My Blog Post".to_string(),
|
||||
//! published: None, // Use database default (e.g., false)
|
||||
//! created_at: None, // Use database default (e.g., NOW())
|
||||
//! internal_note: Some("Draft".to_string()),
|
||||
//! author_id: 42,
|
||||
//! };
|
||||
//!
|
||||
//! // Create the entity in the database
|
||||
//! // Create the entity in the database (instance method on PostDefault)
|
||||
//! let created_post = post_default.create(&pool).await?;
|
||||
//! println!("Created post with ID: {}", created_post.id);
|
||||
//! ```
|
||||
//!
|
||||
//! ### Rules and Limitations
|
||||
//! ### Defaultable Rules and Limitations
|
||||
//!
|
||||
//! - **Option fields cannot be marked as defaultable**: If a field is already
|
||||
//! `Option<T>`, you cannot mark it with `#[georm(defaultable)]`. This prevents
|
||||
//! `Option<Option<T>>` types.
|
||||
//! `Option<Option<T>>` types and causes a compile-time error.
|
||||
//! - **Field visibility is preserved**: The generated defaultable struct maintains
|
||||
//! the same field visibility (`pub`, `pub(crate)`, private) as the original
|
||||
//! struct.
|
||||
//! - **ID fields can be defaultable**: It's common to mark ID fields as
|
||||
//! defaultable when they are auto-generated serials in PostgreSQL.
|
||||
//! the same field visibility (`pub`, `pub(crate)`, private) as the original struct.
|
||||
//! - **ID fields can be defaultable**: It's common to mark ID fields as defaultable
|
||||
//! when they are auto-generated serials in PostgreSQL.
|
||||
//! - **Only generates when needed**: The defaultable struct is only generated if
|
||||
//! at least one field is marked as defaultable.
|
||||
//!
|
||||
//! ## Composite Primary Keys
|
||||
//! ## Relationships
|
||||
//!
|
||||
//! Georm supports composite primary keys by marking multiple fields with
|
||||
//! `#[georm(id)]`:
|
||||
//! Georm supports comprehensive relationship modeling with two approaches: field-level
|
||||
//! relationships for foreign keys and struct-level relationships for reverse lookups.
|
||||
//! Each relationship method call executes a separate database query.
|
||||
//!
|
||||
//! ### Field-Level Relationships (Foreign Keys)
|
||||
//!
|
||||
//! Use the `relation` attribute on foreign key fields to generate lookup methods:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(sqlx::FromRow, Georm)]
|
||||
//! #[georm(table = "user_roles")]
|
||||
//! pub struct UserRole {
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "posts")]
|
||||
//! pub struct Post {
|
||||
//! #[georm(id)]
|
||||
//! user_id: i32,
|
||||
//! #[georm(id)]
|
||||
//! role_id: i32,
|
||||
//! assigned_at: chrono::DateTime<chrono::Utc>,
|
||||
//! id: i32,
|
||||
//! 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)
|
||||
//! })]
|
||||
//! author_id: i32,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! When multiple fields are marked as ID fields, Georm automatically generates a
|
||||
//! composite ID struct:
|
||||
//! **Generated instance method**: `post.get_author(pool).await? -> sqlx::Result<Author>`
|
||||
//!
|
||||
//! For nullable relationships:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // Generated automatically by the macro
|
||||
//! pub struct UserRoleId {
|
||||
//! pub user_id: i32,
|
||||
//! pub role_id: i32,
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "posts")]
|
||||
//! pub struct Post {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! title: String,
|
||||
//! #[georm(relation = {
|
||||
//! entity = Category,
|
||||
//! table = "categories",
|
||||
//! name = "category",
|
||||
//! nullable = true // Allows NULL values
|
||||
//! })]
|
||||
//! category_id: Option<i32>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This allows you to use the generated ID struct with all Georm methods:
|
||||
//! **Generated instance method**: `post.get_category(pool).await? -> sqlx::Result<Option<Category>>`
|
||||
//!
|
||||
//! Since `remote_id` and `nullable` have default values, this is equivalent:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // Find by composite key
|
||||
//! let id = UserRoleId { user_id: 1, role_id: 2 };
|
||||
//! let user_role = UserRole::find(&pool, &id).await?;
|
||||
//!
|
||||
//! // Delete by composite key
|
||||
//! UserRole::delete_by_id(&pool, &id).await?;
|
||||
//!
|
||||
//! // Get composite ID from instance
|
||||
//! let user_role = UserRole { user_id: 1, role_id: 2, assigned_at: chrono::Utc::now() };
|
||||
//! let id = user_role.get_id(); // Returns UserRoleId
|
||||
//! #[georm(relation = { entity = Author, table = "authors", name = "author" })]
|
||||
//! author_id: i32,
|
||||
//! ```
|
||||
//!
|
||||
//! ### Composite Key Limitations
|
||||
//! #### Non-Standard Primary Key References
|
||||
//!
|
||||
//! - **Relationships not supported**: Entities with composite primary keys cannot
|
||||
//! yet define relationships (one-to-one, one-to-many, many-to-many) as those
|
||||
//! features require single-field primary keys.
|
||||
//! - **ID struct naming**: The generated ID struct follows the pattern
|
||||
//! `{EntityName}Id`.
|
||||
//! Use `remote_id` to reference tables with non-standard primary key names:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "reviews")]
|
||||
//! pub struct Review {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! #[georm(relation = {
|
||||
//! entity = Book,
|
||||
//! table = "books",
|
||||
//! name = "book",
|
||||
//! remote_id = "ident" // Book uses 'ident' instead of 'id'
|
||||
//! })]
|
||||
//! book_id: i32,
|
||||
//! content: String,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Field-Level Relationship Attributes
|
||||
//!
|
||||
//! | 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 | No | `false` |
|
||||
//!
|
||||
//! ### Struct-Level Relationships (Reverse Lookups)
|
||||
//!
|
||||
//! Define relationships at the struct level to query related entities that reference this entity.
|
||||
//! These generate separate database queries for each method call.
|
||||
//!
|
||||
//! #### One-to-One Relationships
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[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)]
|
||||
//! id: i32,
|
||||
//! username: String,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **Generated instance method**: `user.get_profile(pool).await? -> sqlx::Result<Option<Profile>>`
|
||||
//!
|
||||
//! #### One-to-Many Relationships
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[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)]
|
||||
//! id: i32,
|
||||
//! name: String,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **Generated instance methods**:
|
||||
//! - `author.get_posts(pool).await? -> sqlx::Result<Vec<Post>>`
|
||||
//! - `author.get_comments(pool).await? -> sqlx::Result<Vec<Comment>>`
|
||||
//!
|
||||
//! #### 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)
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[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)]
|
||||
//! id: i32,
|
||||
//! 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)]
|
||||
//! id: i32,
|
||||
//! name: String,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **Generated instance methods**:
|
||||
//! - `book.get_genres(pool).await? -> sqlx::Result<Vec<Genre>>`
|
||||
//! - `genre.get_books(pool).await? -> sqlx::Result<Vec<Book>>`
|
||||
//!
|
||||
//! #### Struct-Level Relationship Attributes
|
||||
//!
|
||||
//! | 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"` |
|
||||
//! | `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
|
||||
//!
|
||||
//! As with field-level relationships, `remote_id` is optional and defaults to `"id"`:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[georm(
|
||||
//! table = "users",
|
||||
//! one_to_many = [{ entity = Post, name = "posts", table = "posts" }]
|
||||
//! )]
|
||||
//! ```
|
||||
//!
|
||||
//! #### Complex Relationship Example
|
||||
//!
|
||||
//! Here's a comprehensive example showing multiple relationship types:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[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)]
|
||||
//! id: i32,
|
||||
//! title: String,
|
||||
//! content: String,
|
||||
//!
|
||||
//! // Field-level relationship (foreign key)
|
||||
//! #[georm(relation = {
|
||||
//! entity = Author,
|
||||
//! table = "authors",
|
||||
//! name = "author"
|
||||
//! })]
|
||||
//! author_id: i32,
|
||||
//!
|
||||
//! // Nullable field-level relationship
|
||||
//! #[georm(relation = {
|
||||
//! entity = Category,
|
||||
//! table = "categories",
|
||||
//! name = "category",
|
||||
//! nullable = true
|
||||
//! })]
|
||||
//! category_id: Option<i32>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **Generated instance methods**:
|
||||
//! - `post.get_author(pool).await? -> sqlx::Result<Author>` (from field relation)
|
||||
//! - `post.get_category(pool).await? -> sqlx::Result<Option<Category>>` (nullable field relation)
|
||||
//! - `post.get_comments(pool).await? -> sqlx::Result<Vec<Comment>>` (one-to-many)
|
||||
//! - `post.get_tags(pool).await? -> sqlx::Result<Vec<Tag>>` (many-to-many)
|
||||
//!
|
||||
//! ## Error Handling
|
||||
//!
|
||||
//! All Georm methods return `sqlx::Result<T>` which can contain:
|
||||
//!
|
||||
//! - **Database errors**: Connection issues, constraint violations, etc.
|
||||
//! - **Not found errors**: When `find()` operations return `None`
|
||||
//! - **Compile-time errors**: Invalid SQL, type mismatches, schema validation failures
|
||||
//!
|
||||
//! ### Compile-Time Validations
|
||||
//!
|
||||
//! Georm performs several validations at compile time:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! // ❌ Compile error: No ID field specified
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "invalid")]
|
||||
//! pub struct Invalid {
|
||||
//! name: String, // Missing #[georm(id)]
|
||||
//! }
|
||||
//!
|
||||
//! // ❌ Compile error: Option<T> cannot be defaultable
|
||||
//! #[derive(Georm)]
|
||||
//! #[georm(table = "invalid")]
|
||||
//! pub struct Invalid {
|
||||
//! #[georm(id)]
|
||||
//! id: i32,
|
||||
//! #[georm(defaultable)] // Error: would create Option<Option<String>>
|
||||
//! optional_field: Option<String>,
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Attribute Reference
|
||||
//!
|
||||
//! ### Struct-Level Attributes
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[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
|
||||
//!
|
||||
//! ```ignore
|
||||
//! #[georm(id)] // Mark as primary key (required on at least one field)
|
||||
//! #[georm(defaultable)] // Mark as defaultable field (database default/auto-generated)
|
||||
//! #[georm(relation = { /* ... */ })] // Define foreign key relationship
|
||||
//! ```
|
||||
//!
|
||||
//! ## Performance Characteristics
|
||||
//!
|
||||
//! - **Zero runtime overhead**: All SQL is generated at compile time
|
||||
//! - **No eager loading**: Each relationship method executes a separate query
|
||||
//! - **Prepared statements**: All queries use parameter binding for optimal performance
|
||||
//! - **Database round-trips**: CRUD operations use RETURNING clause to minimize round-trips
|
||||
//! - **No N+1 prevention**: Built-in relationships don't prevent N+1 query patterns
|
||||
//!
|
||||
//! ## Limitations
|
||||
//! ### Database
|
||||
//!
|
||||
//! For now, Georm is limited to PostgreSQL. Other databases may be supported in
|
||||
//! the future, such as Sqlite or MySQL, but that is not the case yet.
|
||||
//! ### Database Support
|
||||
//!
|
||||
//! ## Identifiers
|
||||
//! Georm is currently limited to PostgreSQL. Other databases may be supported in
|
||||
//! the future, such as SQLite or MySQL, but that is not the case yet.
|
||||
//!
|
||||
//! ### Identifiers
|
||||
//!
|
||||
//! Identifiers, or primary keys from the point of view of the database, may
|
||||
//! be simple types recognized by SQLx or composite keys (multiple fields marked
|
||||
//! with `#[georm(id)]`). Single primary keys cannot be arrays, and optionals are
|
||||
//! only supported in one-to-one relationships when explicitly marked as nullables.
|
||||
//!
|
||||
//! ### Current Limitations
|
||||
//!
|
||||
//! - **Composite key relationships**: Entities with composite primary keys cannot define relationships
|
||||
//! - **Single table per entity**: No table inheritance or polymorphism support
|
||||
//! - **No advanced queries**: No complex WHERE clauses or joins beyond relationships
|
||||
//! - **No eager loading**: Each relationship call is a separate database query
|
||||
//! - **No field-based queries**: No `find_by_{field_name}` methods generated automatically
|
||||
//! - **PostgreSQL only**: No support for other database systems
|
||||
//!
|
||||
//! ## Generated Code
|
||||
//!
|
||||
//! Georm automatically generates:
|
||||
//! - `sqlx::FromRow` implementation (no need to derive manually)
|
||||
//! - Composite ID structs for multi-field primary keys
|
||||
//! - Defaultable companion structs for entities with defaultable fields
|
||||
//! - Relationship methods for accessing related entities
|
||||
//! - All CRUD operations with proper PostgreSQL optimizations
|
||||
|
||||
pub use georm_macros::Georm;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user