diff --git a/src/defaultable.rs b/src/defaultable.rs index 4094ee9..ffacfb5 100644 --- a/src/defaultable.rs +++ b/src/defaultable.rs @@ -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` 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, // Database default: NOW() +/// author_id: i32, // Required field +/// } +/// +/// // Generated automatically: +/// // pub struct PostDefault { +/// // pub id: Option, +/// // pub title: String, +/// // pub published: Option, +/// // pub created_at: Option>, +/// // pub author_id: i32, +/// // } +/// // +/// // impl Defaultable 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, +/// // pub title: String, +/// // pub(crate) internal_status: Option, // Preserved +/// // private_field: Option, // Preserved +/// // } +/// ``` +/// +/// ## Limitations and Rules +/// +/// - **Option fields cannot be defaultable**: Fields that are already `Option` cannot +/// be marked with `#[georm(defaultable)]` to prevent `Option>` types +/// - **Compile-time validation**: Attempts to mark `Option` 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 { - /// 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> + Send; + ) -> impl std::future::Future> + Send + where + Self: Sized; } diff --git a/src/georm.rs b/src/georm.rs index e43e575..0f441ce 100644 --- a/src/georm.rs +++ b/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, +/// } +/// +/// // Generated: pub struct UserRoleId { pub user_id: i32, pub role_id: i32 } +/// // Trait: impl Georm 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` 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 { - /// 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)` - 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>> + 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 { 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 { 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 { 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 { 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> + 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> + 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; } diff --git a/src/lib.rs b/src/lib.rs index db1b3c8..c80fcb5 100644 --- a/src/lib.rs +++ b/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::find_all(pool)` - Get all records, returns `Vec` +//! - `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, //! } //! ``` //! -//! 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`. +//! ### 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`. 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::get_books(&self, pool: &sqlx::PgPool) -> Vec` -//! -//! 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, // Has database default +//! created_at: chrono::DateTime, // 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`: +//! This generates a companion `PostDefault` struct where defaultable fields become `Option`: //! //! ```ignore //! // Generated automatically by the macro @@ -298,6 +154,7 @@ //! pub title: String, // Required field stays the same //! pub published: Option, // Can be None to use database default //! pub created_at: Option>, // Can be None +//! pub(crate) internal_note: Option, // 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`, you cannot mark it with `#[georm(defaultable)]`. This prevents -//! `Option>` types. +//! `Option>` 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, +//! 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` +//! +//! 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, //! } //! ``` //! -//! This allows you to use the generated ID struct with all Georm methods: +//! **Generated instance method**: `post.get_category(pool).await? -> sqlx::Result>` +//! +//! 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>` +//! +//! #### 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>` +//! - `author.get_comments(pool).await? -> sqlx::Result>` +//! +//! #### 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>` +//! - `genre.get_books(pool).await? -> sqlx::Result>` +//! +//! #### 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, +//! } +//! ``` +//! +//! **Generated instance methods**: +//! - `post.get_author(pool).await? -> sqlx::Result` (from field relation) +//! - `post.get_category(pool).await? -> sqlx::Result>` (nullable field relation) +//! - `post.get_comments(pool).await? -> sqlx::Result>` (one-to-many) +//! - `post.get_tags(pool).await? -> sqlx::Result>` (many-to-many) +//! +//! ## Error Handling +//! +//! All Georm methods return `sqlx::Result` 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 cannot be defaultable +//! #[derive(Georm)] +//! #[georm(table = "invalid")] +//! pub struct Invalid { +//! #[georm(id)] +//! id: i32, +//! #[georm(defaultable)] // Error: would create Option> +//! optional_field: Option, +//! } +//! ``` +//! +//! ## 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;