georm/
lib.rs

1//! # Georm
2//!
3//! A simple, type-safe PostgreSQL ORM built on SQLx with zero runtime overhead.
4//!
5//! ## Quick Start
6//!
7//! ```ignore
8//! use georm::Georm;
9//!
10//! // Note: No need to derive FromRow - Georm generates it automatically
11//! #[derive(Georm)]
12//! #[georm(table = "users")]
13//! pub struct User {
14//!     #[georm(id)]
15//!     id: i32,
16//!     username: String,
17//!     email: String,
18//! }
19//!
20//! // Use generated methods
21//! let user = User::find(&pool, &1).await?;      // Static method
22//! let all_users = User::find_all(&pool).await?; // Static method
23//! user.update(&pool).await?;                    // Instance method
24//! ```
25//!
26//! ## Core CRUD Operations
27//!
28//! ### Static Methods (called on the struct type)
29//! - `Entity::find(pool, &id)` - Find by primary key, returns `Option<Entity>`
30//! - `Entity::find_all(pool)` - Get all records, returns `Vec<Entity>`
31//! - `Entity::delete_by_id(pool, &id)` - Delete by ID, returns affected row count
32//!
33//! ### Instance Methods (called on entity objects)
34//! - `entity.create(pool)` - Insert new record, returns created entity with database-generated values
35//! - `entity.update(pool)` - Update existing record, returns updated entity with fresh database state
36//! - `entity.create_or_update(pool)` - True PostgreSQL upsert using `ON CONFLICT`, returns final entity
37//! - `entity.delete(pool)` - Delete this record, returns affected row count
38//! - `entity.get_id()` - Get reference to the entity's ID (`&Id` for simple keys, owned for composite)
39//!
40//! ```ignore
41//! // Static methods
42//! let user = User::find(&pool, &1).await?.unwrap();
43//! let all_users = User::find_all(&pool).await?;
44//! let deleted_count = User::delete_by_id(&pool, &1).await?;
45//!
46//! // Instance methods
47//! let new_user = User { id: 0, username: "alice".to_string(), email: "alice@example.com".to_string() };
48//! let created = new_user.create(&pool).await?;        // Returns entity with actual generated ID
49//! let updated = created.update(&pool).await?;         // Returns entity with fresh database state
50//! let deleted_count = updated.delete(&pool).await?;
51//! ```
52//!
53//! ### PostgreSQL Optimizations
54//!
55//! Georm leverages PostgreSQL-specific features for performance and reliability:
56//!
57//! - **RETURNING clause**: All `INSERT` and `UPDATE` operations use `RETURNING *` to capture database-generated values (sequences, defaults, triggers)
58//! - **True upserts**: `create_or_update()` uses `INSERT ... ON CONFLICT ... DO UPDATE` for atomic upsert operations
59//! - **Prepared statements**: All queries use parameter binding for security and performance
60//! - **Compile-time verification**: SQLx macros verify all generated SQL against your database schema at compile time
61//!
62//! ## Primary Keys and Identifiers
63//!
64//! ### Simple Primary Keys
65//!
66//! Primary key fields can have any name (not just "id"):
67//!
68//! ```ignore
69//! #[derive(Georm)]
70//! #[georm(table = "books")]
71//! pub struct Book {
72//!     #[georm(id)]
73//!     ident: i32,  // Custom field name for primary key
74//!     title: String,
75//! }
76//!
77//! // Works the same way
78//! let book = Book::find(&pool, &1).await?;
79//! ```
80//!
81//! ### Composite Primary Keys
82//!
83//! Mark multiple fields with `#[georm(id)]` for composite keys:
84//!
85//! ```ignore
86//! #[derive(Georm)]
87//! #[georm(table = "user_roles")]
88//! pub struct UserRole {
89//!     #[georm(id)]
90//!     user_id: i32,
91//!     #[georm(id)]
92//!     role_id: i32,
93//!     assigned_at: chrono::DateTime<chrono::Utc>,
94//! }
95//! ```
96//!
97//! This automatically generates a composite ID struct following the `{EntityName}Id` pattern:
98//!
99//! ```ignore
100//! // Generated automatically by the macro
101//! pub struct UserRoleId {
102//!     pub user_id: i32,
103//!     pub role_id: i32,
104//! }
105//! ```
106//!
107//! Usage with composite keys:
108//!
109//! ```ignore
110//! // Static methods work with generated ID structs
111//! let id = UserRoleId { user_id: 1, role_id: 2 };
112//! let user_role = UserRole::find(&pool, &id).await?;
113//! UserRole::delete_by_id(&pool, &id).await?;
114//!
115//! // Instance methods work the same way
116//! let role = UserRole { user_id: 1, role_id: 2, assigned_at: chrono::Utc::now() };
117//! let created = role.create(&pool).await?;
118//! let id = created.get_id(); // Returns owned UserRoleId for composite keys
119//! ```
120//!
121//! ### Composite Key Limitations
122//!
123//! - **Relationships not supported**: Entities with composite primary keys cannot
124//!   yet define relationships (one-to-one, one-to-many, many-to-many)
125//! - **ID struct naming**: Generated ID struct follows pattern `{EntityName}Id` (not customizable)
126//!
127//! ## Defaultable Fields
128//!
129//! Use `#[georm(defaultable)]` for fields with database defaults or auto-generated values:
130//!
131//! ```ignore
132//! #[derive(Georm)]
133//! #[georm(table = "posts")]
134//! pub struct Post {
135//!     #[georm(id, defaultable)]
136//!     id: i32,                    // Auto-generated serial
137//!     title: String,              // Required field
138//!     #[georm(defaultable)]
139//!     published: bool,            // Has database default
140//!     #[georm(defaultable)]
141//!     created_at: chrono::DateTime<chrono::Utc>, // DEFAULT NOW()
142//!     #[georm(defaultable)]
143//!     pub(crate) internal_note: String,          // Field visibility preserved
144//!     author_id: i32,             // Required field
145//! }
146//! ```
147//!
148//! This generates a companion `PostDefault` struct where defaultable fields become `Option<T>`:
149//!
150//! ```ignore
151//! // Generated automatically by the macro
152//! pub struct PostDefault {
153//!     pub id: Option<i32>,        // Can be None for auto-generation
154//!     pub title: String,          // Required field stays the same
155//!     pub published: Option<bool>, // Can be None to use database default
156//!     pub created_at: Option<chrono::DateTime<chrono::Utc>>, // Can be None
157//!     pub(crate) internal_note: Option<String>,  // Visibility preserved
158//!     pub author_id: i32,         // Required field stays the same
159//! }
160//!
161//! impl Defaultable<i32, Post> for PostDefault {
162//!     async fn create(&self, pool: &sqlx::PgPool) -> sqlx::Result<Post>;
163//! }
164//! ```
165//!
166//! ### Usage Example
167//!
168//! ```ignore
169//! use georm::{Georm, Defaultable};
170//!
171//! // Create a post with some fields using database defaults
172//! let post_default = PostDefault {
173//!     id: None,                   // Let database auto-generate
174//!     title: "My Blog Post".to_string(),
175//!     published: None,            // Use database default (e.g., false)
176//!     created_at: None,           // Use database default (e.g., NOW())
177//!     internal_note: Some("Draft".to_string()),
178//!     author_id: 42,
179//! };
180//!
181//! // Create the entity in the database (instance method on PostDefault)
182//! let created_post = post_default.create(&pool).await?;
183//! println!("Created post with ID: {}", created_post.id);
184//! ```
185//!
186//! ### Defaultable Rules and Limitations
187//!
188//! - **Option fields cannot be marked as defaultable**: If a field is already
189//!   `Option<T>`, you cannot mark it with `#[georm(defaultable)]`. This prevents
190//!   `Option<Option<T>>` types and causes a compile-time error.
191//! - **Field visibility is preserved**: The generated defaultable struct maintains
192//!   the same field visibility (`pub`, `pub(crate)`, private) as the original struct.
193//! - **ID fields can be defaultable**: It's common to mark ID fields as defaultable
194//!   when they are auto-generated serials in PostgreSQL.
195//! - **Only generates when needed**: The defaultable struct is only generated if
196//!   at least one field is marked as defaultable.
197//!
198//! ## Relationships
199//!
200//! Georm supports comprehensive relationship modeling with two approaches: field-level
201//! relationships for foreign keys and struct-level relationships for reverse lookups.
202//! Each relationship method call executes a separate database query.
203//!
204//! ### Field-Level Relationships (Foreign Keys)
205//!
206//! Use the `relation` attribute on foreign key fields to generate lookup methods:
207//!
208//! ```ignore
209//! #[derive(Georm)]
210//! #[georm(table = "posts")]
211//! pub struct Post {
212//!     #[georm(id)]
213//!     id: i32,
214//!     title: String,
215//!     #[georm(relation = {
216//!         entity = Author,        // Target entity type
217//!         table = "authors",      // Target table name
218//!         name = "author",        // Method name (generates get_author)
219//!         remote_id = "id",       // Target table's key column (default: "id")
220//!         nullable = false        // Whether relationship can be null (default: false)
221//!     })]
222//!     author_id: i32,
223//! }
224//! ```
225//!
226//! **Generated instance method**: `post.get_author(pool).await? -> sqlx::Result<Author>`
227//!
228//! For nullable relationships:
229//!
230//! ```ignore
231//! #[derive(Georm)]
232//! #[georm(table = "posts")]
233//! pub struct Post {
234//!     #[georm(id)]
235//!     id: i32,
236//!     title: String,
237//!     #[georm(relation = {
238//!         entity = Category,
239//!         table = "categories",
240//!         name = "category",
241//!         nullable = true         // Allows NULL values
242//!     })]
243//!     category_id: Option<i32>,
244//! }
245//! ```
246//!
247//! **Generated instance method**: `post.get_category(pool).await? -> sqlx::Result<Option<Category>>`
248//!
249//! Since `remote_id` and `nullable` have default values, this is equivalent:
250//!
251//! ```ignore
252//! #[georm(relation = { entity = Author, table = "authors", name = "author" })]
253//! author_id: i32,
254//! ```
255//!
256//! #### Non-Standard Primary Key References
257//!
258//! Use `remote_id` to reference tables with non-standard primary key names:
259//!
260//! ```ignore
261//! #[derive(Georm)]
262//! #[georm(table = "reviews")]
263//! pub struct Review {
264//!     #[georm(id)]
265//!     id: i32,
266//!     #[georm(relation = {
267//!         entity = Book,
268//!         table = "books",
269//!         name = "book",
270//!         remote_id = "ident"     // Book uses 'ident' instead of 'id'
271//!     })]
272//!     book_id: i32,
273//!     content: String,
274//! }
275//! ```
276//!
277//! #### Field-Level Relationship Attributes
278//!
279//! | Attribute    | Description                                          | Required | Default |
280//! |--------------|------------------------------------------------------|----------|---------|
281//! | `entity`     | Target entity type                                   | Yes      | N/A     |
282//! | `name`       | Method name (generates `get_{name}`)                 | Yes      | N/A     |
283//! | `table`      | Target table name                                    | Yes      | N/A     |
284//! | `remote_id`  | Target table's key column                            | No       | `"id"`  |
285//! | `nullable`   | Whether relationship can be null                     | No       | `false` |
286//!
287//! ### Struct-Level Relationships (Reverse Lookups)
288//!
289//! Define relationships at the struct level to query related entities that reference this entity.
290//! These generate separate database queries for each method call.
291//!
292//! #### One-to-One Relationships
293//!
294//! ```ignore
295//! #[derive(Georm)]
296//! #[georm(
297//!     table = "users",
298//!     one_to_one = [{
299//!         entity = Profile,       // Related entity type
300//!         name = "profile",       // Method name (generates get_profile)
301//!         table = "profiles",     // Related table name
302//!         remote_id = "user_id",  // Foreign key in related table
303//!     }]
304//! )]
305//! pub struct User {
306//!     #[georm(id)]
307//!     id: i32,
308//!     username: String,
309//! }
310//! ```
311//!
312//! **Generated instance method**: `user.get_profile(pool).await? -> sqlx::Result<Option<Profile>>`
313//!
314//! #### One-to-Many Relationships
315//!
316//! ```ignore
317//! #[derive(Georm)]
318//! #[georm(
319//!     table = "authors",
320//!     one_to_many = [{
321//!         entity = Post,          // Related entity type
322//!         name = "posts",         // Method name (generates get_posts)
323//!         table = "posts",        // Related table name
324//!         remote_id = "author_id" // Foreign key in related table
325//!     }, {
326//!         entity = Comment,       // Multiple relationships allowed
327//!         name = "comments",
328//!         table = "comments",
329//!         remote_id = "author_id"
330//!     }]
331//! )]
332//! pub struct Author {
333//!     #[georm(id)]
334//!     id: i32,
335//!     name: String,
336//! }
337//! ```
338//!
339//! **Generated instance methods**:
340//! - `author.get_posts(pool).await? -> sqlx::Result<Vec<Post>>`
341//! - `author.get_comments(pool).await? -> sqlx::Result<Vec<Comment>>`
342//!
343//! #### Many-to-Many Relationships
344//!
345//! For many-to-many relationships, specify the link table that connects the entities:
346//!
347//! ```sql
348//! -- Example schema for books and genres
349//! CREATE TABLE books (
350//!     id SERIAL PRIMARY KEY,
351//!     title VARCHAR(200) NOT NULL
352//! );
353//!
354//! CREATE TABLE genres (
355//!     id SERIAL PRIMARY KEY,
356//!     name VARCHAR(100) NOT NULL
357//! );
358//!
359//! CREATE TABLE book_genres (
360//!     book_id INT NOT NULL REFERENCES books(id),
361//!     genre_id INT NOT NULL REFERENCES genres(id),
362//!     PRIMARY KEY (book_id, genre_id)
363//! );
364//! ```
365//!
366//! ```ignore
367//! #[derive(Georm)]
368//! #[georm(
369//!     table = "books",
370//!     many_to_many = [{
371//!         entity = Genre,         // Related entity type
372//!         name = "genres",        // Method name (generates get_genres)
373//!         table = "genres",       // Related table name
374//!         remote_id = "id",       // Primary key in related table (default: "id")
375//!         link = {                // Link table configuration
376//!             table = "book_genres",  // Join table name
377//!             from = "book_id",       // Column referencing this entity
378//!             to = "genre_id"         // Column referencing related entity
379//!         }
380//!     }]
381//! )]
382//! pub struct Book {
383//!     #[georm(id)]
384//!     id: i32,
385//!     title: String,
386//! }
387//!
388//! #[derive(Georm)]
389//! #[georm(
390//!     table = "genres",
391//!     many_to_many = [{
392//!         entity = Book,
393//!         name = "books",
394//!         table = "books",
395//!         link = {
396//!             table = "book_genres",
397//!             from = "genre_id",      // Note: reversed perspective
398//!             to = "book_id"
399//!         }
400//!     }]
401//! )]
402//! pub struct Genre {
403//!     #[georm(id)]
404//!     id: i32,
405//!     name: String,
406//! }
407//! ```
408//!
409//! **Generated instance methods**:
410//! - `book.get_genres(pool).await? -> sqlx::Result<Vec<Genre>>`
411//! - `genre.get_books(pool).await? -> sqlx::Result<Vec<Book>>`
412//!
413//! #### Struct-Level Relationship Attributes
414//!
415//! | Attribute    | Description                                          | Required | Default |
416//! |--------------|------------------------------------------------------|----------|---------|
417//! | `entity`     | Target entity type                                   | Yes      | N/A     |
418//! | `name`       | Method name (generates `get_{name}`)                 | Yes      | N/A     |
419//! | `table`      | Target table name                                    | Yes      | N/A     |
420//! | `remote_id`  | Target table's key column                            | No       | `"id"`  |
421//! | `link.table` | Join table name (many-to-many only)                  | Yes*     | N/A     |
422//! | `link.from`  | Column referencing this entity (many-to-many only)   | Yes*     | N/A     |
423//! | `link.to`    | Column referencing target entity (many-to-many only) | Yes*     | N/A     |
424//!
425//! *Required for many-to-many relationships
426//!
427//! As with field-level relationships, `remote_id` is optional and defaults to `"id"`:
428//!
429//! ```ignore
430//! #[georm(
431//!     table = "users",
432//!     one_to_many = [{ entity = Post, name = "posts", table = "posts" }]
433//! )]
434//! ```
435//!
436//! #### Complex Relationship Example
437//!
438//! Here's a comprehensive example showing multiple relationship types:
439//!
440//! ```ignore
441//! #[derive(Georm)]
442//! #[georm(
443//!     table = "posts",
444//!     one_to_many = [{
445//!         entity = Comment,
446//!         name = "comments",
447//!         table = "comments",
448//!         remote_id = "post_id"
449//!     }],
450//!     many_to_many = [{
451//!         entity = Tag,
452//!         name = "tags",
453//!         table = "tags",
454//!         link = {
455//!             table = "post_tags",
456//!             from = "post_id",
457//!             to = "tag_id"
458//!         }
459//!     }]
460//! )]
461//! pub struct Post {
462//!     #[georm(id)]
463//!     id: i32,
464//!     title: String,
465//!     content: String,
466//!
467//!     // Field-level relationship (foreign key)
468//!     #[georm(relation = {
469//!         entity = Author,
470//!         table = "authors",
471//!         name = "author"
472//!     })]
473//!     author_id: i32,
474//!
475//!     // Nullable field-level relationship
476//!     #[georm(relation = {
477//!         entity = Category,
478//!         table = "categories",
479//!         name = "category",
480//!         nullable = true
481//!     })]
482//!     category_id: Option<i32>,
483//! }
484//! ```
485//!
486//! **Generated instance methods**:
487//! - `post.get_author(pool).await? -> sqlx::Result<Author>` (from field relation)
488//! - `post.get_category(pool).await? -> sqlx::Result<Option<Category>>` (nullable field relation)
489//! - `post.get_comments(pool).await? -> sqlx::Result<Vec<Comment>>` (one-to-many)
490//! - `post.get_tags(pool).await? -> sqlx::Result<Vec<Tag>>` (many-to-many)
491//!
492//! ## Error Handling
493//!
494//! All Georm methods return `sqlx::Result<T>` which can contain:
495//!
496//! - **Database errors**: Connection issues, constraint violations, etc.
497//! - **Not found errors**: When `find()` operations return `None`
498//! - **Compile-time errors**: Invalid SQL, type mismatches, schema validation failures
499//!
500//! ### Compile-Time Validations
501//!
502//! Georm performs several validations at compile time:
503//!
504//! ```ignore
505//! // ❌ Compile error: No ID field specified
506//! #[derive(Georm)]
507//! #[georm(table = "invalid")]
508//! pub struct Invalid {
509//!     name: String,  // Missing #[georm(id)]
510//! }
511//!
512//! // ❌ Compile error: Option<T> cannot be defaultable
513//! #[derive(Georm)]
514//! #[georm(table = "invalid")]
515//! pub struct Invalid {
516//!     #[georm(id)]
517//!     id: i32,
518//!     #[georm(defaultable)]  // Error: would create Option<Option<String>>
519//!     optional_field: Option<String>,
520//! }
521//! ```
522//!
523//! ## Attribute Reference
524//!
525//! ### Struct-Level Attributes
526//!
527//! ```ignore
528//! #[georm(
529//!     table = "table_name",                   // Required: database table name
530//!     one_to_one = [{ /* ... */ }],           // Optional: one-to-one relationships
531//!     one_to_many = [{ /* ... */ }],          // Optional: one-to-many relationships
532//!     many_to_many = [{ /* ... */ }]          // Optional: many-to-many relationships
533//! )]
534//! ```
535//!
536//! ### Field-Level Attributes
537//!
538//! ```ignore
539//! #[georm(id)]                               // Mark as primary key (required on at least one field)
540//! #[georm(defaultable)]                      // Mark as defaultable field (database default/auto-generated)
541//! #[georm(relation = { /* ... */ })]         // Define foreign key relationship
542//! ```
543//!
544//! ## Performance Characteristics
545//!
546//! - **Zero runtime overhead**: All SQL is generated at compile time
547//! - **No eager loading**: Each relationship method executes a separate query
548//! - **Prepared statements**: All queries use parameter binding for optimal performance
549//! - **Database round-trips**: CRUD operations use RETURNING clause to minimize round-trips
550//! - **No N+1 prevention**: Built-in relationships don't prevent N+1 query patterns
551//!
552//! ## Limitations
553//!
554//! ### Database Support
555//!
556//! Georm is currently limited to PostgreSQL. Other databases may be supported in
557//! the future, such as SQLite or MySQL, but that is not the case yet.
558//!
559//! ### Identifiers
560//!
561//! Identifiers, or primary keys from the point of view of the database, may
562//! be simple types recognized by SQLx or composite keys (multiple fields marked
563//! with `#[georm(id)]`). Single primary keys cannot be arrays, and optionals are
564//! only supported in one-to-one relationships when explicitly marked as nullables.
565//!
566//! ### Current Limitations
567//!
568//! - **Composite key relationships**: Entities with composite primary keys cannot define relationships
569//! - **Single table per entity**: No table inheritance or polymorphism support
570//! - **No advanced queries**: No complex WHERE clauses or joins beyond relationships
571//! - **No eager loading**: Each relationship call is a separate database query
572//! - **No field-based queries**: No `find_by_{field_name}` methods generated automatically
573//! - **PostgreSQL only**: No support for other database systems
574//!
575//! ## Generated Code
576//!
577//! Georm automatically generates:
578//! - `sqlx::FromRow` implementation (no need to derive manually)
579//! - Composite ID structs for multi-field primary keys
580//! - Defaultable companion structs for entities with defaultable fields
581//! - Relationship methods for accessing related entities
582//! - All CRUD operations with proper PostgreSQL optimizations
583
584pub use georm_macros::Georm;
585
586mod georm;
587pub use georm::Georm;
588mod defaultable;
589pub use defaultable::Defaultable;