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;