Introduction
Fabrique is a SQL-first, type-safe, ergonomic database toolkit for Rust. Your SQL knowledge transfers directly, the compiler catches errors before your code runs, and derive macros eliminate the boilerplate.
Core Values
- SQL-first — If you know SQL, you know Fabrique. Queries map directly to the SQL you’d write by hand.
- Type-safe — Column references, joins, and query structure are validated at compile time. Errors surface in your IDE, not in production.
- Ergonomic —
#[derive(Model)]generates everything you need. Fluent APIs and sensible defaults keep your code concise.
Quick Example
Derive Model on a struct to map it to a database table:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
fn main() {}
Fabrique maps Product to the products table and generates type-safe column
constants. Use them to build queries that read like SQL:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let deals = Product::query()
.r#where(Product::IN_STOCK, "=", true)
.r#where(Product::PRICE_CENTS, "<=", 5000)
.order_by(Product::PRICE_CENTS, "ASC")
.limit(10)
.get(&pool)
.await?;
Ok(())
}
Common operations like inserting a record have convenience methods that wrap the query builder, so you don’t have to assemble it yourself:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let anvil = Product {
id: Uuid::new_v4(),
name: "Anvil 3000".to_string(),
price_cents: 4999,
in_stock: true,
};
anvil.create(&pool).await?;
Ok(())
}
New to Fabrique? Start with the Getting Started tutorial.
Where to Go Next
- Getting Started — Build your first Fabrique-powered app in minutes
- Concepts — Understand models, the query builder, and relations
- Cookbook — Recipes for common tasks
- API Reference — Full API documentation on docs.rs
Getting Started
This tutorial walks you through adding Fabrique to an existing e-commerce application. You’ll learn how to convert plain Rust structs into database-backed models and implement service functions that your views can call.
Prerequisites
- Rust 1.75 or later
- A supported database (SQLite, PostgreSQL, or MySQL)
- Basic knowledge of SQL and async Rust
Scenario
You’re working on an e-commerce website. The frontend team has defined the API contract, and you need to implement the persistence layer. Your task is to make all the service functions work with a database.
The Starting Point
Here’s what you’re given — a Product struct and service function stubs that
need implementation:
use fabrique::prelude::*;
use uuid::Uuid;
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
// Find a product by its ID
pub async fn find_product_by_id(
pool: &Pool<Backend>,
id: Uuid,
) -> Result<Product, Box<dyn std::error::Error>> {
unimplemented!()
}
// List all products currently in stock
pub async fn list_available_products(
pool: &Pool<Backend>,
) -> Result<Vec<Product>, Box<dyn std::error::Error>> {
unimplemented!()
}
// Create a new product
pub async fn create_product(
pool: &Pool<Backend>,
name: String,
price_cents: i32,
) -> Result<Product, Box<dyn std::error::Error>> {
unimplemented!()
}
// Update a product's price
pub async fn update_product_price(
pool: &Pool<Backend>,
id: Uuid,
new_price_cents: i32,
) -> Result<Product, Box<dyn std::error::Error>> {
unimplemented!()
}
// Delete a product
pub async fn delete_product(
pool: &Pool<Backend>,
id: Uuid,
) -> Result<(), Box<dyn std::error::Error>> {
unimplemented!()
}
Let’s implement each function using Fabrique.
Database Setup
First, create the products table:
CREATE TABLE products (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
price_cents INTEGER NOT NULL,
in_stock BOOLEAN NOT NULL DEFAULT 1
);
Adding Fabrique
Add dependencies to Cargo.toml:
[dependencies]
fabrique = "0.1"
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid"] }
tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["v4"] }
Now derive Model on the Product struct:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
fn main() {}
Fabrique automatically:
- Maps
Productto theproductstable - Uses
idas the primary key - Generates column constants (
Product::ID,Product::NAME, etc.) - Provides query and persistence methods
Implementing the Service Functions
Finding a Product by ID
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
pub async fn find_product_by_id(
pool: &Pool<Backend>,
id: Uuid,
) -> Result<Product, fabrique::Error> {
Product::find(pool, id).await
}
fn main() {}
This queries SELECT * FROM products WHERE id = $1 and returns the matching
record. If no product is found, it returns an error.
Listing Available Products
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
pub async fn list_available_products(
pool: &Pool<Backend>,
) -> Result<Vec<Product>, fabrique::Error> {
Product::query()
.r#where(Product::IN_STOCK, "=", true)
.get(pool)
.await
}
fn main() {}
The query builder provides a fluent API. Here we:
- Start a query with
Product::query() - Add a WHERE clause with
.r#where() - Execute and collect results with
.get(pool)
Column constants like Product::IN_STOCK are type-safe — passing the wrong type
won’t compile.
Creating a Product
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
pub async fn create_product(
pool: &Pool<Backend>,
name: String,
price_cents: i32,
) -> Result<Product, fabrique::Error> {
let product = Product {
id: Uuid::new_v4(),
name,
price_cents,
in_stock: true,
};
product.create(pool).await
}
fn main() {}
The create method inserts the record and returns it. If a record with the same
primary key already exists, it returns an error.
Updating a Product’s Price
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
pub async fn update_product_price(
pool: &Pool<Backend>,
id: Uuid,
new_price_cents: i32,
) -> Result<Product, fabrique::Error> {
let mut product: Product = Product::query()
.r#where(Product::ID, "=", id)
.first_or_fail(pool)
.await?;
product.price_cents = new_price_cents;
product.save(pool).await
}
fn main() {}
The pattern is: fetch, modify, save. The save method performs an upsert — it
inserts if the record is new, or updates if it exists.
Deleting a Product
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
pub async fn delete_product(
pool: &Pool<Backend>,
id: Uuid,
) -> Result<(), fabrique::Error> {
Product::destroy(pool, id).await
}
fn main() {}
The destroy method deletes by primary key without fetching the record first.
The Complete Implementation
Here’s the full working code:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
pub async fn find_product_by_id(
pool: &Pool<Backend>,
id: Uuid,
) -> Result<Product, fabrique::Error> {
Product::find(pool, id).await
}
pub async fn list_available_products(
pool: &Pool<Backend>,
) -> Result<Vec<Product>, fabrique::Error> {
Product::query()
.r#where(Product::IN_STOCK, "=", true)
.get(pool)
.await
}
pub async fn create_product(
pool: &Pool<Backend>,
name: String,
price_cents: i32,
) -> Result<Product, fabrique::Error> {
let product = Product {
id: Uuid::new_v4(),
name,
price_cents,
in_stock: true,
};
product.create(pool).await
}
pub async fn update_product_price(
pool: &Pool<Backend>,
id: Uuid,
new_price_cents: i32,
) -> Result<Product, fabrique::Error> {
let mut product: Product = Product::query()
.r#where(Product::ID, "=", id)
.first_or_fail(pool)
.await?;
product.price_cents = new_price_cents;
product.save(pool).await
}
pub async fn delete_product(
pool: &Pool<Backend>,
id: Uuid,
) -> Result<(), fabrique::Error> {
Product::destroy(pool, id).await
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create an in-memory SQLite database and run migrations
let pool = sqlx::SqlitePool::connect(":memory:").await?;
sqlx::migrate!("../migrations/sqlite").run(&pool).await?;
// Create products
let anvil = create_product(&pool, "Anvil 3000".to_string(), 4999).await?;
let rocket = create_product(&pool, "Rocket Skates".to_string(), 14999).await?;
println!("Created: {} and {}", anvil.name, rocket.name);
// Find a specific product
let found = find_product_by_id(&pool, anvil.id).await?;
println!("Found: {}", found.name);
// List available products
let available = list_available_products(&pool).await?;
println!("Available: {} products", available.len());
// Update a price
let updated = update_product_price(&pool, anvil.id, 3999).await?;
let price = updated.price_cents as f64 / 100.0;
println!("Updated {} to ${:.2}", updated.name, price);
// Delete a product
delete_product(&pool, rocket.id).await?;
println!("Deleted Rocket Skates");
Ok(())
}
What’s Next: Testing
Now that the service functions work, you’ll want to test them. The Testing with Fabrique tutorial shows how to write database-backed tests with isolated databases and generated test data.
Summary
You’ve learned how to integrate Fabrique into an existing application:
- Derive
Modelon your structs to enable database operations - Use the query builder with
first_or_failfor primary key lookups - Use the query builder with
r#wherefor filtered queries - Use
createandsavefor persistence - Use
destroyfor deletion
Next Steps
- Learn about Relations to model users, orders, and products together
- Explore the Query Builder for complex queries
- Read about Transactions for atomic operations
- Learn how to test your code with factories and isolated databases
Building an E-commerce App
This tutorial builds on Getting Started by adding relations between models. You’ll implement a complete e-commerce backend with users, orders, and order lines.
Prerequisites
- Completed the Getting Started tutorial
- Understanding of foreign keys and table relationships
What You’ll Build
A multi-model e-commerce system with:
- Users who place orders
- Orders that belong to users
- Order lines linking orders to products (many-to-many)
- Service functions to create orders and fetch user order history
The Starting Point
Your team has expanded the database schema and defined these service stubs:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}
pub struct Order {
pub id: Uuid,
pub user_id: Uuid,
pub status: String,
}
pub struct OrderLine {
pub order_id: Uuid,
pub product_id: Uuid,
pub quantity: i32,
pub unit_price_cents: i32,
}
pub struct Product {
pub id: Uuid,
}
// Get a user with all their orders
pub async fn get_user_with_orders(
pool: &Pool<Backend>,
user_id: Uuid,
) -> Result<(User, Vec<Order>), Box<dyn std::error::Error>> {
unimplemented!()
}
// Create an order for a user with multiple products
pub async fn create_order(
pool: &Pool<Backend>,
user_id: Uuid,
items: Vec<(Uuid, i32, i32)>, // (product_id, quantity, unit_price_cents)
) -> Result<Order, Box<dyn std::error::Error>> {
unimplemented!()
}
// Get all products in an order
pub async fn get_order_products(
pool: &Pool<Backend>,
order_id: Uuid,
) -> Result<Vec<Product>, Box<dyn std::error::Error>> {
unimplemented!()
}
fn main() {}
Database Schema
Extend your database with users, orders, and order lines:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending'
);
CREATE TABLE order_lines (
order_id UUID NOT NULL REFERENCES orders(id),
product_id UUID NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL DEFAULT 1,
unit_price_cents INTEGER NOT NULL, -- captured at order time
PRIMARY KEY (order_id, product_id)
);
Note that unit_price_cents is stored in order_lines even though products have
a price. This captures the price at the moment of purchase — if a product’s price
changes later, historical orders still reflect what the customer actually paid.
Defining Models with Relations
The User Model
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
/// A user has many orders
pub orders: HasMany<Order>,
}
fn main() {}
The HasMany<Order> field tells Fabrique that a user can have multiple orders.
This field isn’t stored in the database — it generates a method to fetch related
orders.
The Order Model
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[derive(Clone, Debug, Factory, Model)]
#[fabrique(table = "order_lines")]
pub struct OrderLine {
#[fabrique(primary_key, belongs_to = "Order")]
pub order_id: Uuid,
#[fabrique(primary_key, belongs_to = "Product")]
pub product_id: Uuid,
pub quantity: i32,
pub unit_price_cents: i32,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
/// This order belongs to a user
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
/// Products in this order, linked through OrderLine
#[fabrique(through = "OrderLine")]
pub products: HasMany<Product>,
}
fn main() {}
Key points:
#[fabrique(belongs_to = "User")]marks the foreign key relationship#[fabrique(through = "OrderLine")]defines a many-to-many relationship via the join table
The OrderLine Model (Join Table)
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[derive(Clone, Debug, Factory, Model)]
#[fabrique(table = "order_lines")]
pub struct OrderLine {
#[fabrique(primary_key, belongs_to = "Order")]
pub order_id: Uuid,
#[fabrique(primary_key, belongs_to = "Product")]
pub product_id: Uuid,
pub quantity: i32,
pub unit_price_cents: i32,
}
fn main() {}
This join table has:
- A composite primary key (
order_id,product_id) - Foreign keys to both
OrderandProduct - Additional data (
quantity,unit_price_cents)
Implementing the Service Functions
Getting a User with Their Orders
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
// -----------------------------------------------------------------------------
// Models
// -----------------------------------------------------------------------------
// --snip--
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
// -----------------------------------------------------------------------------
// Service functions
// -----------------------------------------------------------------------------
/// Fetches a user and all their orders.
pub async fn get_user_with_orders(
pool: &Pool<Backend>,
user_id: Uuid,
) -> Result<(User, Vec<Order>), fabrique::Error> {
let user: User = User::query()
.r#where(User::ID, "=", user_id)
.first_or_fail(pool)
.await?;
let orders = user.orders().get(pool).await?;
Ok((user, orders))
}
// --snip--
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let user = User::factory().create(&pool).await?;
let (_, orders) = get_user_with_orders(&pool, user.id).await?;
assert_eq!(orders.len(), 0);
Ok(())
}
The orders() method returns a query builder pre-filtered to this user’s orders.
You can add more conditions:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
// -----------------------------------------------------------------------------
// Models
// -----------------------------------------------------------------------------
// --snip--
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
// -----------------------------------------------------------------------------
// Service functions
// -----------------------------------------------------------------------------
// --snip--
/// Fetches only the pending orders for a user.
pub async fn get_user_pending_orders(
pool: &Pool<Backend>,
user_id: Uuid,
) -> Result<Vec<Order>, fabrique::Error> {
let user: User = User::query()
.r#where(User::ID, "=", user_id)
.first_or_fail(pool)
.await?;
user.orders()
.r#where(Order::STATUS, "=", "pending")
.get(pool)
.await
}
// --snip--
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Insert a user with 2 pending orders and 1 shipped order
let user = User::factory()
.has_orders(Order::factory().status("pending".to_string()), 2)
.has_orders(Order::factory().status("shipped".to_string()), 1)
.create(&pool).await?;
// get_user_pending_orders should only return pending orders
let pending = get_user_pending_orders(&pool, user.id).await?;
assert_eq!(pending.len(), 2);
assert!(pending.iter().all(|o| o.status == "pending"));
Ok(())
}
Creating an Order with Items
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
// -----------------------------------------------------------------------------
// Models
// -----------------------------------------------------------------------------
// --snip--
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
#[fabrique(through = "OrderLine")]
pub products: HasMany<Product>,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}
#[derive(Clone, Debug, Factory, Model)]
#[fabrique(table = "order_lines")]
pub struct OrderLine {
#[fabrique(primary_key, belongs_to = "Order")]
pub order_id: Uuid,
#[fabrique(primary_key, belongs_to = "Product")]
pub product_id: Uuid,
pub quantity: i32,
pub unit_price_cents: i32,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
// -----------------------------------------------------------------------------
// Service functions
// -----------------------------------------------------------------------------
// --snip--
/// Creates an order for a user with the given items.
pub async fn create_order(
pool: &Pool<Backend>,
user_id: Uuid,
items: Vec<(Uuid, i32, i32)>, // (product_id, quantity, unit_price_cents)
) -> Result<Order, fabrique::Error> {
// Create the order
let order = Order {
id: Uuid::new_v4(),
user_id,
status: "pending".to_string(),
products: HasMany::new(),
};
let order = order.create(pool).await?;
// Create order lines for each item
for (product_id, quantity, unit_price_cents) in items {
let line = OrderLine {
order_id: order.id,
product_id,
quantity,
unit_price_cents,
};
line.create(pool).await?;
}
Ok(order)
}
// --snip--
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Insert a user and two products in the database
let user = User::factory().create(&pool).await?;
let product_a = Product::factory().price_cents(1000).create(&pool).await?;
let product_b = Product::factory().price_cents(2500).create(&pool).await?;
// create_order should return a pending order with both products
let order_items = vec![
(product_a.id, 2, product_a.price_cents),
(product_b.id, 1, product_b.price_cents),
];
let order = create_order(&pool, user.id, order_items).await?;
assert_eq!(order.status, "pending");
let products = order.products().get(&pool).await?;
assert_eq!(products.len(), 2);
Ok(())
}
Getting Products in an Order
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
// -----------------------------------------------------------------------------
// Models
// -----------------------------------------------------------------------------
// --snip--
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[derive(Clone, Debug, Factory, Model)]
#[fabrique(table = "order_lines")]
pub struct OrderLine {
#[fabrique(primary_key, belongs_to = "Order")]
pub order_id: Uuid,
#[fabrique(primary_key, belongs_to = "Product")]
pub product_id: Uuid,
pub quantity: i32,
pub unit_price_cents: i32,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
#[fabrique(through = "OrderLine")]
pub products: HasMany<Product>,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}
// -----------------------------------------------------------------------------
// Service functions
// -----------------------------------------------------------------------------
// --snip--
/// Returns all products in an order.
pub async fn get_order_products(
pool: &Pool<Backend>,
order_id: Uuid,
) -> Result<Vec<Product>, fabrique::Error> {
let order = Order::find(pool, order_id).await?;
order.products().get(pool).await
}
// --snip--
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Insert a user, a product, an order, and an order line
let user = User::factory().create(&pool).await?;
let product = Product::factory().create(&pool).await?;
let order = Order {
id: Uuid::new_v4(),
user_id: user.id,
status: "pending".to_string(),
products: HasMany::new(),
};
let order = order.create(&pool).await?;
let line = OrderLine {
order_id: order.id,
product_id: product.id,
quantity: 1,
unit_price_cents: 999,
};
line.create(&pool).await?;
// get_order_products should return the product linked to the order
let products = get_order_products(&pool, order.id).await?;
assert_eq!(products.len(), 1);
assert_eq!(products[0].id, product.id);
Ok(())
}
The products() method automatically joins through the order_lines table.
The Complete Implementation
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
// -----------------------------------------------------------------------------
// Models
// -----------------------------------------------------------------------------
/// A user who can place orders.
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
/// A product available for purchase.
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
/// An order placed by a user.
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
#[fabrique(through = "OrderLine")]
pub products: HasMany<Product>,
}
/// A line item linking an order to a product.
#[derive(Clone, Debug, Factory, Model)]
#[fabrique(table = "order_lines")]
pub struct OrderLine {
#[fabrique(primary_key, belongs_to = "Order")]
pub order_id: Uuid,
#[fabrique(primary_key, belongs_to = "Product")]
pub product_id: Uuid,
pub quantity: i32,
pub unit_price_cents: i32,
}
// -----------------------------------------------------------------------------
// Service functions
// -----------------------------------------------------------------------------
/// Fetches a user and all their orders.
pub async fn get_user_with_orders(
pool: &Pool<Backend>,
user_id: Uuid,
) -> Result<(User, Vec<Order>), fabrique::Error> {
let user = User::find(pool, user_id).await?;
let orders = user.orders().get(pool).await?;
Ok((user, orders))
}
/// Creates an order for a user with the given items.
pub async fn create_order(
pool: &Pool<Backend>,
user_id: Uuid,
items: Vec<(Uuid, i32, i32)>,
) -> Result<Order, fabrique::Error> {
let order = Order {
id: Uuid::new_v4(),
user_id,
status: "pending".to_string(),
products: HasMany::new(),
};
let order = order.create(pool).await?;
for (product_id, quantity, unit_price_cents) in items {
let line = OrderLine {
order_id: order.id,
product_id,
quantity,
unit_price_cents,
};
line.create(pool).await?;
}
Ok(order)
}
/// Returns all products in an order.
pub async fn get_order_products(
pool: &Pool<Backend>,
order_id: Uuid,
) -> Result<Vec<Product>, fabrique::Error> {
let order = Order::find(pool, order_id).await?;
order.products().get(pool).await
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Insert a user and two products in the database
let user = User::factory()
.name("Wile E. Coyote".to_string())
.email("wile@acme.example".to_string())
.create(&pool).await?;
let anvil = Product::factory()
.name("Anvil 3000".to_string())
.price_cents(4999)
.create(&pool).await?;
let rocket = Product::factory()
.name("Rocket Skates".to_string())
.price_cents(14999)
.create(&pool).await?;
// create_order should return a pending order
let order = create_order(
&pool,
user.id,
vec![
(anvil.id, 2, anvil.price_cents),
(rocket.id, 1, rocket.price_cents),
],
)
.await?;
assert_eq!(order.status, "pending");
// get_user_with_orders should return the created order
let (_, orders) = get_user_with_orders(&pool, user.id).await?;
assert_eq!(orders.len(), 1);
// get_order_products should return both products
let products = get_order_products(&pool, order.id).await?;
assert_eq!(products.len(), 2);
Ok(())
}
Summary
You’ve learned how to model related data with Fabrique:
belongs_tomarks foreign key fieldsHasMany<T>declares one-to-many relationshipsthroughenables many-to-many relationships via join tables- Composite primary keys work naturally with
#[fabrique(primary_key)] - Relation methods return query builders for lazy loading
Next Steps
- Read the Relations concept for more details
- Learn how to test your code with factories and isolated databases
- Read about Transactions to make order creation atomic
- Explore Error Handling for robust error management
Testing with Fabrique
The previous tutorials built service functions for an e-commerce app. This tutorial shows how to test them — with isolated databases, generated test data, and relations.
Prerequisites
- Completed Getting Started and Building an E-commerce App
- The models and service functions from those tutorials
Your First Database Test
Each test needs a fresh database with tables ready. The
#[fabrique::test] macro handles this — it wraps
#[sqlx::test]
and automatically selects the migration directory for the
active backend:
use fabrique::prelude::*;
#[fabrique::test]
async fn test_name(pool: Pool<Backend>) {
// pool is a fresh, migrated database
// unique to this test — no interference
}
The pool parameter is an isolated database created for this
test only. When the test ends, the database is dropped.
Let’s test list_available_products from the Getting Started
tutorial. First, add Factory to the derive list if you
haven’t already:
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
Now write the test using the Arrange / Act / Assert pattern:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
pub async fn list_available_products(
pool: &Pool<Backend>,
) -> Result<Vec<Product>, fabrique::Error> {
Product::query()
.r#where(Product::IN_STOCK, "=", true)
.get(pool)
.await
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Arrange — two in stock, one out of stock
Product::factory()
.in_stock(true)
.create(&pool).await?;
Product::factory()
.in_stock(true)
.create(&pool).await?;
Product::factory()
.in_stock(false)
.create(&pool).await?;
// Act
let available = list_available_products(&pool).await?;
// Assert
assert_eq!(available.len(), 2);
Ok(())
}
Two things to notice:
- We only set
in_stock— the field our test cares about.id,name, andprice_centsare generated automatically. - Each test gets its own database — no cleanup needed, no interference between tests.
Focusing on What Matters
Compare the factory approach to manual construction:
// Manual — every field, even irrelevant ones
let product = Product {
id: Uuid::new_v4(),
name: "Irrelevant".to_string(),
price_cents: 0,
in_stock: true,
};
product.create(&pool).await.unwrap();
// Factory — only what matters
Product::factory()
.in_stock(true)
.create(&pool).await.unwrap();
With a 4-field struct the difference is modest. With 15 fields, it becomes significant — and the test’s intent is immediately clear: this test is about stock availability, nothing else.
Testing Relations
Let’s test get_user_pending_orders from the E-commerce
tutorial. We need a user with orders in different statuses.
Sharing a Parent
Create a user first, then create orders that reference it:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let user = User::factory().create(&pool).await?;
Order::factory()
.for_user(user.clone())
.status("pending".to_string())
.create(&pool).await?;
Order::factory()
.for_user(user.clone())
.status("shipped".to_string())
.create(&pool).await?;
// Both orders belong to the same user
let orders = user.orders().get(&pool).await?;
assert_eq!(orders.len(), 2);
Ok(())
}
Both orders share the same user because we pass the
instance. Passing User::factory() instead would create
a new user for each order — useful when you need distinct
parents.
Creating from the Parent Side
has_orders creates children in a single call:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let user = User::factory()
.has_orders(
Order::factory().status("pending".to_string()),
3,
)
.create(&pool).await?;
let orders = user.orders().get(&pool).await?;
assert_eq!(orders.len(), 3);
assert!(orders.iter().all(|o| o.status == "pending"));
Ok(())
}
A Complete Test
Putting it together — test that get_user_pending_orders
returns only pending orders:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Debug, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
}
pub async fn get_user_pending_orders(
pool: &Pool<Backend>,
user_id: Uuid,
) -> Result<Vec<Order>, fabrique::Error> {
let user: User = User::query()
.r#where(User::ID, "=", user_id)
.first_or_fail(pool)
.await?;
user.orders()
.r#where(Order::STATUS, "=", "pending")
.get(pool)
.await
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Arrange — a user with mixed order statuses
let user = User::factory()
.has_orders(
Order::factory().status("pending".to_string()),
2,
)
.has_orders(
Order::factory().status("shipped".to_string()),
1,
)
.create(&pool).await?;
// Act
let pending = get_user_pending_orders(&pool, user.id)
.await?;
// Assert
assert_eq!(pending.len(), 2);
assert!(pending.iter().all(|o| o.status == "pending"));
Ok(())
}
The test reads like a specification: given a user with 2
pending and 1 shipped order, get_user_pending_orders returns
exactly the 2 pending ones.
Generating Realistic Data
By default, factories fill fields with random values. For more
realistic data, the faker attribute specifies a generator
from the fake crate:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use fabrique::fake::faker::name::en::Name;
use fabrique::fake::faker::internet::en::SafeEmail;
use uuid::Uuid;
#[derive(Model, Factory)]
pub struct User {
id: Uuid,
#[fabrique(faker = "Name()")]
name: String,
#[fabrique(faker = "SafeEmail()")]
email: String,
}
fn main() {}
Common expressions:
| Expression | Example Output |
|---|---|
Name() | “John Smith” |
SafeEmail() | “john@example.com” |
CompanyName() | “Acme Industries” |
CityName() | “Springfield” |
(1..100) | Random integer 1–99 |
(100..10000) | Random integer for cents |
Import generators from fabrique::fake::faker:
extern crate fabrique;
use fabrique::fake::faker::company::en::CompanyName;
use fabrique::fake::faker::address::en::CityName;
use fabrique::fake::faker::lorem::en::Sentence;
fn main() {}
Each factory call generates fresh values — records are unique without hardcoded strings. See the fake crate documentation for the full list of generators.
Summary
You’ve learned how to test a Fabrique application:
#[fabrique::test]gives each test an isolated, migrated database- Factories generate test data — set only the fields that matter for your assertion
for_<relation>andhas_<relation>set up related records without manual foreign key wiringfakerproduces realistic data when random values aren’t enough
Next Steps
- Explore the Factories concept for the full factory API
- Read about Transactions to test transactional code
- See the Query Builder for more complex test assertions
Models
A model is a Rust struct mapped to a database table. Deriving Model on a
struct generates the table mapping, column constants, and convenience methods
for querying and persisting data.
Fabrique favors convention over configuration to minimize boilerplate. Table names and primary keys are inferred from your struct definition. When your schema doesn’t follow these conventions, attributes let you opt in explicitly.
Defining a Model
Create a struct and derive Model:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
pub struct Product {
id: Uuid,
name: String,
price_cents: i32,
}
fn main() {}
Table Names
By convention, the snake_case plural name of the struct is used as the table
name. Product maps to products, RocketShoe maps to rocket_shoes.
To override the convention, use the table attribute:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
#[fabrique(table = "acme_products")]
pub struct Product {
id: Uuid,
name: String,
}
fn main() {}
Primary Keys
By convention, Fabrique uses a field named id as the primary key. To use a
different field, annotate it with #[fabrique(primary_key)]:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
pub struct Product {
#[fabrique(primary_key)]
product_id: Uuid,
name: String,
}
fn main() {}
If neither an id field nor a #[fabrique(primary_key)] annotation exists,
compilation fails:
error: Missing primary key, either add an id column or mark an existing
column as primary
--> src/product.rs:4:8
|
4 | struct Product {
| ^^^^^^^
Composite primary keys are supported by annotating multiple fields:
extern crate fabrique;
extern crate sqlx;
use fabrique::prelude::*;
#[derive(Model)]
pub struct OrderLine {
#[fabrique(primary_key)]
order_id: i32,
#[fabrique(primary_key)]
product_id: i32,
quantity: i32,
}
fn main() {}
Column Constants
Deriving Model generates a constant for each field:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
pub struct Product {
id: Uuid,
name: String,
price_cents: i32,
}
// Generated constants:
// Product::ID
// Product::NAME
// Product::PRICE_CENTS
fn main() {
let _ = Product::ID;
let _ = Product::NAME;
let _ = Product::PRICE_CENTS;
}
Each constant represents a specific column and its associated type at compile
time, with no runtime cost. This is what enables type-safe queries — the
compiler knows exactly which column Product::PRICE_CENTS refers to, and will
reject any operation with an incompatible type. See the
Query Builder for details.
Type Conversions
Common Rust types (String, i32, bool, Uuid, …) map directly to
SQL columns — see Database for the full list. For custom
types, the #[fabrique(as = "Type")] attribute tells Fabrique to convert
the field through a Rust type that maps to SQL:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug)]
pub enum Material { Wood, Metal, Rubber }
impl From<Material> for String {
fn from(m: Material) -> String {
match m {
Material::Wood => "wood",
Material::Metal => "metal",
Material::Rubber => "rubber",
}.to_string()
}
}
impl TryFrom<String> for Material {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.as_str() {
"wood" => Ok(Material::Wood),
"metal" => Ok(Material::Metal),
"rubber" => Ok(Material::Rubber),
_ => Err(format!("unknown material: {}", s)),
}
}
}
#[derive(Model)]
pub struct Product {
id: Uuid,
name: String,
#[fabrique(as = "String")]
material: Material, // stored as TEXT
}
fn main() {}
Fabrique uses TryFrom in both directions. Implementing From
satisfies this and is the common choice for writing:
#[derive(Clone, Debug)]
pub enum Material { Wood, Metal, Rubber }
// Writing: Material → String (infallible)
impl From<Material> for String {
// --snip--
fn from(m: Material) -> String {
match m {
Material::Wood => "wood".to_string(),
Material::Metal => "metal".to_string(),
Material::Rubber => "rubber".to_string(),
}
}
}
// Reading: String → Material (fallible)
impl TryFrom<String> for Material {
// --snip--
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.as_str() {
"wood" => Ok(Material::Wood),
"metal" => Ok(Material::Metal),
"rubber" => Ok(Material::Rubber),
other => Err(format!("unknown material: {other}")),
}
}
}
fn main() {}
If conversion fails when reading, Fabrique returns
Error::Conversion. See Database for details.
Convenience Methods
Deriving Model also generates convenience methods that wrap the
query builder for common operations. For instance:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
extern crate tokio;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Insert a new record
let anvil = Product {
id: Uuid::new_v4(),
name: "Anvil 3000".to_string(),
price_cents: 4999,
};
let anvil: Product = anvil.create(&pool).await?;
// Fetch by primary key
let anvil: Product = Product::find(&pool, anvil.id).await?;
// Upsert (insert or update on PK conflict)
let anvil: Product = anvil.save(&pool).await?;
// Delete by primary key, no instance needed
Product::destroy(&pool, anvil.id).await?;
Ok(())
}
See the API reference for the full list. For more complex queries, or if you prefer an explicit approach, use the Query Builder directly.
Query Builder
The query builder is generic over a model. It infers the table name from the model declaration, so you never specify it manually — no risk of referencing the wrong table. Subsequent operations follow the same logic: columns are resolved from the model, and the compiler validates types at each step.
Rather than writing QueryBuilder::<Product>::new(), models provide
direct entry points:
Product::query()— starts a SELECT pathProduct::insert()— starts an INSERTProduct::update()— starts an UPDATE
Why
query()and notselect()? In SQL,SELECTcomes first. Fabrique inverts this: joins and filters come before column selection. This is what enables type-safe selects — the compiler needs to know which models are joined before it can validate which columns you pick.select()is reserved for choosing columns explicitly (see Column Selection).
Reading Data
Product::query() starts a SELECT query. Chain methods to add
clauses, then execute the query by passing a connection:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory().price_cents(3000).in_stock(true).create(&pool).await?;
let deals: Vec<Product> = Product::query()
.r#where(Product::IN_STOCK, "=", true)
.r#where(Product::PRICE_CENTS, "<=", 5000)
.get(&pool)
.await?;
Ok(())
}
.get() executes the query asynchronously and returns all matching
records. Multiple .r#where() calls are combined with AND. All
executors take a connection — pool or transaction. See the
API reference for the full executor
specification, and Database for pool vs transaction
handling.
Null Checks
r#where takes a column, an operator, and a value — but NULL isn’t
a value of the column’s type. Rather than compromising type safety,
Fabrique provides dedicated methods:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub deleted_at: Option<String>,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
User::factory().deleted_at(None).create(&pool).await?;
let active = User::query()
.where_null(User::DELETED_AT)
.get(&pool)
.await?;
let archived = User::query()
.where_not_null(User::DELETED_AT)
.get(&pool)
.await?;
Ok(())
}
Tip: For systematic soft delete handling, see Soft Delete and Restore Records.
Ordering and Pagination
order_by sorts results by a column. limit and offset control
how many records are returned and where to start:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory().in_stock(true).create(&pool).await?;
let page = Product::query()
.r#where(Product::IN_STOCK, "=", true)
.order_by(Product::PRICE_CENTS, "ASC")
.limit(20)
.offset(40)
.get(&pool)
.await?;
Ok(())
}
Single Record
When you expect a single record, first returns an Option<T>,
and first_or_fail returns T directly — raising
Error::NotFound rather than leaving you to unwrap or convert to
a Result:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory().price_cents(3000).create(&pool).await?;
let cheapest: Option<Product> = Product::query()
.r#where(Product::PRICE_CENTS, "<=", 5000)
.first(&pool)
.await?;
let cheapest: Product = Product::query()
.r#where(Product::PRICE_CENTS, "<=", 5000)
.first_or_fail(&pool)
.await?;
Ok(())
}
Writing Data
Inserting
Product::insert() starts an INSERT query. .set() assigns
column values, then .execute() runs the query without returning
data:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::insert()
.set(Product::ID, Uuid::new_v4())
.set(Product::NAME, "Anvil 3000")
.set(Product::PRICE_CENTS, 4999)
.set(Product::IN_STOCK, true)
.execute(&pool)
.await?;
Ok(())
}
When you need the inserted record back, chain .returning() before
the executor — this avoids a separate SELECT roundtrip:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let anvil: Product = Product::insert()
.set(Product::ID, Uuid::new_v4())
.set(Product::NAME, "Anvil 3000")
.set(Product::PRICE_CENTS, 4999)
.set(Product::IN_STOCK, true)
.returning()
.first_or_fail(&pool)
.await?;
Ok(())
}
Updating
Product::update() starts an UPDATE query. Combine .set() with
.r#where() to target specific records:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory().price_cents(30).create(&pool).await?;
Product::update()
.set(Product::PRICE_CENTS, 100)
.r#where(Product::PRICE_CENTS, "<", 50)
.execute(&pool)
.await?;
Ok(())
}
.returning() works here too — use it to get the updated records
back without a separate query.
Note:
model.create()andmodel.save()(see Models) wrap this query builder internally. They return the persisted record usingRETURNING. MySQL doesn’t supportRETURNING— Fabrique emulates the behavior with two queries (INSERT then SELECT).
Joins
join::<T>() adds an INNER JOIN to a related model. The compiler
enforces that T is joinable from the current query context — if
no relationship is declared between the models, it won’t compile:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Factory, Model)]
pub struct Order {
pub id: Uuid,
pub status: String,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Order::factory().create(&pool).await?;
// Both directions work — the relation is declared once
let users = User::query()
.join::<Order>()
.r#where(User::EMAIL, "=", "wile@acme.com")
.get(&pool)
.await?;
let orders = Order::query()
.join::<User>()
.get(&pool)
.await?;
Ok(())
}
When a table isn’t directly related but reachable through an
intermediate table, join_through chains the join. The
intermediate must be joined first — just like in SQL, the join
chain must be valid at each step:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}
#[derive(Clone, Factory, Model)]
pub struct Order {
pub id: Uuid,
pub status: String,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
#[fabrique(through = "OrderLine")]
pub products: HasMany<Product>,
}
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[derive(Clone, Factory, Model)]
#[fabrique(table = "order_lines")]
pub struct OrderLine {
#[fabrique(primary_key, belongs_to = "Order")]
pub order_id: Uuid,
#[fabrique(primary_key, belongs_to = "Product")]
pub product_id: Uuid,
pub quantity: i32,
pub unit_price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Order::factory().create(&pool).await?;
// Order → OrderLine (direct) → Product (through OrderLine)
let orders = Order::query()
.join::<OrderLine>()
.join_through::<Product, OrderLine, _>()
.get(&pool)
.await?;
Ok(())
}
This is standard SQL join semantics — Fabrique adds compile-time validation on top. See Relations for how to declare relationships between models.
Named Joins
When a model has multiple foreign keys to the same parent (e.g.
sender_id and recipient_id both referencing users),
join_as::<Model, Alias>() joins the table under a SQL alias.
Each alias is a marker type generated by the alias attribute
(see Relations > Aliases):
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User { id: Uuid, name: String }
#[derive(Factory, Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
#[fabrique::doctest]
async fn main(
pool: Pool<Backend>,
) -> Result<(), fabrique::Error> {
let messages = Message::query()
.join_as::<User, Sender>()
.join_as::<User, Recipient>()
.where_on::<Sender, _, _, _, _>(
User::NAME, "=", "Alice".to_string(),
)
.order_by_on::<Recipient, _, _, _>(
User::NAME, "ASC",
)
.get(&pool)
.await?;
Ok(())
}
This generates:
SELECT messages.*
FROM messages
JOIN users AS sender
ON sender.id = messages.sender_id
JOIN users AS recipient
ON recipient.id = messages.recipient_id
WHERE sender.name = $1
ORDER BY recipient.name ASC
where_on, where_null_on, where_not_null_on, and
order_by_on work like their unnamed counterparts but qualify
the column through the alias. See
Handling Multiple belongs_to for a full example.
Column Selection
When a query executes, Fabrique maps each database row into a
struct instance. select_as::<Model, _>() specifies which model
to build:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory().create(&pool).await?;
let products: Vec<Product> = Product::query()
.select_as::<Product, _>()
.get(&pool)
.await?;
Ok(())
}
By default, the query builder infers the root model — so
select_as can be omitted. This is what all the examples above
do:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory().create(&pool).await?;
// Equivalent — Product is inferred
let products: Vec<Product> = Product::query()
.get(&pool)
.await?;
Ok(())
}
After a join, select_as switches the output to a different
model:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Factory, Model)]
pub struct Order {
pub id: Uuid,
pub status: String,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Order::factory().create(&pool).await?;
let orders: Vec<Order> = User::query()
.join::<Order>()
.select_as::<Order, _>()
.r#where(User::EMAIL, "=", "wile@acme.com")
.get(&pool)
.await?;
Ok(())
}
To select specific columns, use select with a tuple of column
constants — the return type matches:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory().create(&pool).await?;
let rows: Vec<(String, i32)> = Product::query()
.select((Product::NAME, Product::PRICE_CENTS))
.get(&pool)
.await?;
Ok(())
}
This works across joined models too, as long as the join is present:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Factory, Model)]
pub struct Order {
pub id: Uuid,
pub status: String,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Order::factory().create(&pool).await?;
let rows: Vec<(String, String)> = User::query()
.join::<Order>()
.select((User::NAME, Order::STATUS))
.get(&pool)
.await?;
Ok(())
}
The compiler verifies that each selected column belongs to a model present in the query — selecting from an unjoined model won’t compile.
Next: Relations — declaring relationships between models.
Relations
At its core, SQL has a single relationship mechanism: the
foreign key. Fabrique models this directly — a belongs_to
attribute on a foreign key column connects two models, and the
compiler handles the rest (bidirectional joins, type validation).
On top of this foundation, a few annotations provide the ergonomics you’d expect from a traditional ORM — inverse declarations, many-to-many through tables — without multiplying relationship types.
Declaring a Relationship
A relationship starts with a foreign key. Annotate the field with
belongs_to to tell Fabrique which model it references:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
pub struct User { id: Uuid }
#[derive(Model)]
pub struct Order {
id: Uuid,
#[fabrique(belongs_to = "User")]
customer_id: Uuid,
}
fn main() {}
Here, Order holds the foreign key (customer_id) that references
User’s primary key (id). From this single declaration, Fabrique
generates:
BelongsTo<User>forOrder— exposes the foreign key column for queries and factoriesJoinable<User>forOrder— enablesOrder::query().join::<User>()Joinable<Order>forUser— enablesUser::query().join::<Order>()
Joins work in both directions regardless of which model holds the foreign key.
The Inverse
The parent side can declare a HasMany<T> field to get a
convenience method for loading related records. This field is not
stored in the database — it’s a marker:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
pub struct Order {
id: Uuid,
#[fabrique(belongs_to = "User")]
customer_id: Uuid,
}
#[derive(Model)]
pub struct User {
id: Uuid,
name: String,
orders: HasMany<Order>,
}
fn main() {}
This generates an orders() method on User that returns a
query builder filtering orders by the user’s
primary key. Fabrique resolves the foreign key column by looking
at the BelongsTo<User> trait on Order.
Through Tables
When two models are related through an intermediate table, use the
through attribute on HasMany. The join model must have
belongs_to relationships to both sides:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
pub struct Anvil {
id: Uuid,
name: String,
}
/// The join model must belong to both sides
#[derive(Model)]
#[fabrique(table = "order_lines")]
pub struct OrderLine {
#[fabrique(primary_key, belongs_to = "Order")]
order_id: Uuid,
#[fabrique(primary_key, belongs_to = "Anvil")]
anvil_id: Uuid,
quantity: i32,
}
#[derive(Model)]
pub struct Order {
id: Uuid,
#[fabrique(through = "OrderLine")]
anvils: HasMany<Anvil>,
}
fn main() {}
This generates an anvils() method on Order that joins through
OrderLine to fetch related Anvil records.
Aliases
When a model has multiple foreign keys to the same parent, there
is an ambiguity: Fabrique cannot determine which foreign key to
use for joins or HasMany resolution. The alias attribute
disambiguates each reference:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
pub struct User { id: Uuid, name: String }
#[derive(Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
fn main() {}
Each alias generates a marker type (unit struct) implementing
the Alias trait. From the example above, Fabrique generates:
struct Senderandstruct RecipientBelongsTo<User, Sender>andBelongsTo<User, Recipient>forMessage- Bidirectional
Joinableimpls for each alias
On the parent side, HasMany references the alias to resolve
which foreign key to use:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
#[derive(Model)]
pub struct User {
id: Uuid,
name: String,
#[fabrique(alias = "Sender")]
sent_messages: HasMany<Message>,
#[fabrique(alias = "Recipient")]
received_messages: HasMany<Message>,
}
fn main() {}
In queries, join_as joins the same model multiple times under
different aliases. where_on and order_by_on qualify columns
through a specific alias:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User { id: Uuid, name: String }
#[derive(Factory, Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
#[fabrique::doctest]
async fn main(
pool: Pool<Backend>,
) -> Result<(), fabrique::Error> {
let messages = Message::query()
.join_as::<User, Sender>()
.join_as::<User, Recipient>()
.where_on::<Sender, _, _, _, _>(
User::NAME, "=", "Alice".to_string(),
)
.get(&pool)
.await?;
Ok(())
}
This generates:
SELECT messages.*
FROM messages
JOIN users AS sender ON sender.id = messages.sender_id
JOIN users AS recipient
ON recipient.id = messages.recipient_id
WHERE sender.name = $1
See Handling Multiple belongs_to Relationships for a complete walkthrough covering factories, lazy loading, and ordering.
Next: Query Builder — building and executing queries.
Factories
Factories generate model instances for testing. Instead of manually specifying every attribute, factories provide sensible defaults and let you override only what matters for your specific test case.
Note: Factories will move behind a
testingfeature flag (#105) to prevent accidental use in production code.
The Builder Pattern
Each model with #[derive(Factory)] gets a builder struct that
mirrors its fields. Call Model::factory() to get a builder, set
any fields you care about, then call create() to persist:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Model, Factory)]
pub struct Product {
id: Uuid,
name: String,
price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let product = Product::factory()
.name("Anvil 3000".to_string()) // Override name
.create(&pool) // id and price_cents: defaults
.await?;
assert_eq!(product.name, "Anvil 3000");
Ok(())
}
Fields you don’t set are filled with generated values automatically.
Random Value Generation
By default, factories generate random values for all fields using the fake crate. Each factory instance gets unique data without additional configuration.
For more realistic data, use the faker attribute to specify a
generator:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use fabrique::fake::faker::name::en::Name;
use fabrique::fake::faker::internet::en::SafeEmail;
use uuid::Uuid;
#[derive(Model, Factory)]
pub struct User {
id: Uuid,
#[fabrique(faker = "Name()")]
name: String,
#[fabrique(faker = "SafeEmail()")]
email: String,
#[fabrique(faker = "(18..65)")]
age: i32,
}
fn main() {}
The expression is evaluated each time, ensuring unique values across instances. See the fake crate documentation for the full list of available generators.
Relation Support
Factories understand belongs_to relationships. When a
factory creates a record with a foreign key, it automatically
creates the parent record if none is specified:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Model, Factory)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Model, Factory)]
pub struct Order {
pub id: Uuid,
pub status: String,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// A User is auto-created — no manual setup needed
let order = Order::factory()
.create(&pool)
.await?;
assert_ne!(order.user_id, Uuid::nil());
Ok(())
}
Cyclic relation graphs: If your relation graph contains a cycle, you might wonder whether auto-creation recurses infinitely. It doesn’t — SQL already requires at least one foreign key in the cycle to be nullable, and factories skip optional foreign keys (
Option<Uuid>), which breaks the chain.
Use for_<relation>() to take control — pass a factory
to customize the parent, or a model instance to reuse an
existing one:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Model, Factory)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Model, Factory)]
pub struct Order {
pub id: Uuid,
pub status: String,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Pass a factory: creates a new user with specific attributes
let order = Order::factory()
.for_user(User::factory().name("Wile E.".to_string()))
.create(&pool)
.await?;
// Pass a model: reuses an existing user
let user = User::factory().create(&pool).await?;
let order = Order::factory()
.for_user(user)
.create(&pool)
.await?;
Ok(())
}
This distinction matters when creating multiple records. Passing a factory creates a new parent each time; passing an instance shares the same parent:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Model, Factory)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Model, Factory)]
pub struct Order {
pub id: Uuid,
pub status: String,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// A conversation between two specific people
let wile = User::factory()
.name("Wile E.".to_string())
.create(&pool)
.await?;
let runner = User::factory()
.name("Road Runner".to_string())
.create(&pool)
.await?;
// All orders share the same user — no duplication
for _ in 0..5 {
Order::factory()
.for_user(wile.clone())
.create(&pool)
.await?;
}
Ok(())
}
has_<relation>() works in the other direction — creating
children for a parent:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Model, Factory)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Model, Factory)]
pub struct Order {
pub id: Uuid,
pub status: String,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let user = User::factory()
.has_orders(Order::factory(), 3)
.create(&pool)
.await?;
let orders = user.orders().get(&pool).await?;
assert_eq!(orders.len(), 3);
Ok(())
}
Both combine for complex object graphs in a single call. Missing parents are auto-created at every level of the chain:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Model, Factory)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}
#[derive(Clone, Model, Factory)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[derive(Clone, Model, Factory)]
pub struct Order {
pub id: Uuid,
pub status: String,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
#[fabrique(through = "OrderLine")]
pub products: HasMany<Product>,
pub order_lines: HasMany<OrderLine>,
}
#[derive(Clone, Model, Factory)]
#[fabrique(table = "order_lines")]
pub struct OrderLine {
#[fabrique(primary_key, belongs_to = "Order")]
pub order_id: Uuid,
#[fabrique(primary_key, belongs_to = "Product")]
pub product_id: Uuid,
pub quantity: i32,
pub unit_price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// 1 order, 3 order lines, each with its own product
let order = Order::factory()
.has_order_lines(OrderLine::factory(), 3)
.create(&pool)
.await?;
Ok(())
}
The factory creates records in dependency order: a user and products are auto-created, then order lines with the correct foreign keys, then the order — all from a single chain.
Next: Database — backends, connections, and error handling.
Database
Fabrique builds on sqlx for
all database access. sqlx handles the runtime foundation — connection
pooling, parameterized queries (no risk of SQL injection), and
driver-level communication with the database. Fabrique adds
structurally correct query construction, type-safe column validation,
model mapping, and abstracts away SQL dialect differences (placeholder
syntax, RETURNING emulation on MySQL) on top.
For the internal design of the query builder and how it delegates to sqlx, see Architecture. For query construction, see Query Builder.
Backends
The Backend type alias resolves at compile time to the sqlx
database type selected by a Cargo feature flag (postgres, mysql,
or sqlite). Features are mutually exclusive — this ensures
Pool<Backend> always refers to a single, known database type.
SQL dialects differ across backends — for example, PostgreSQL
supports RETURNING natively while MySQL doesn’t, and placeholder
syntax varies ($1 vs ?). Fabrique abstracts these differences
internally so that model code and queries stay the same regardless
of backend.
Common Rust types (String, i32, bool, Uuid, …) map
directly to SQL columns through sqlx. For the full list of supported
type mappings, see the sqlx documentation:
Postgres,
MySQL,
SQLite.
For custom Rust types, see
Models > Type Conversions.
Connections
Every Fabrique operation takes a connection — either a
Pool<Backend> or a transaction. The API is uniform: the same
query works with both, and code remains backend-agnostic:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}
#[fabrique::doctest]
async fn main(
pool: Pool<Backend>,
) -> Result<(), fabrique::Error> {
async fn count_users(
pool: &Pool<Backend>,
) -> Result<usize, fabrique::Error> {
let users = User::all(pool).await?;
Ok(users.len())
}
let count = count_users(&pool).await?;
Ok(())
}
Transactions are acquired from the pool with pool.begin() and
committed explicitly. A transaction that is dropped without
commit() rolls back automatically:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let id = Uuid::new_v4();
Product::factory()
.id(id).in_stock(true).price_cents(5000)
.create(&pool).await?;
let mut tx = pool.begin().await?;
Product::update()
.set(Product::IN_STOCK, false)
.r#where(Product::PRICE_CENTS, ">", 1000)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
sqlx’s Acquire trait abstracts over both pools and transactions,
enabling generic function signatures that accept either.
Error Handling
All Fabrique operations return Result<T, fabrique::Error>. Two
variants come up frequently.
NotFound is returned when a query expects exactly one record but
none matches — typically from find or first_or_fail. When an
empty result is a valid outcome rather than an error, first
returns Option<T> instead.
Conversion is returned when a
type conversion fails — for example,
a database column contains "unknown" but the Rust enum only
accepts "active" or "inactive". The error carries the field
name, source and target types, and whether the conversion failed
reading from or writing to the database.
See the API reference for the full
Error type.
SQLite for Testing
SQLite’s in-memory mode makes it a natural choice for testing: no external database setup, fast execution, and complete isolation between tests.
Enable the sqlite feature in your project:
[dependencies]
fabrique = { version = "0.2", features = ["sqlite"] }
Note: Backend features are mutually exclusive. If your production environment uses PostgreSQL or MySQL, use Cargo feature flags or a workspace setup to switch backends between development and deployment — do not enable two backend features simultaneously.
The #[fabrique::doctest] macro leverages SQLite’s in-memory
mode for documentation examples.
Next: Architecture — the internal design of the query builder.
Architecture
The query builder is the heart of Fabrique. Models, factories, and convenience methods all funnel through it — it is the central engine that everything else builds on. This is what makes Fabrique a toolkit rather than an ORM: the model is a declaration interface that provides ergonomics and abstraction, but the query builder does the actual work.
Fabrique builds on sqlx for database access. sqlx handles connection pooling, parameter binding, and SQL injection protection. Fabrique constructs structurally correct queries, then delegates execution to sqlx.
Two Layers
The query builder is split into two layers:
-
Layer 1 (SQL) constructs valid SQL from string identifiers. It knows about SQL structure —
SELECT,WHERE,JOIN— but nothing about your models. Table and column names are plain&str. -
Layer 2 (Model) wraps Layer 1 with model awareness. It resolves table names, column names, and types from your model declarations. This is the layer you interact with —
Product::query()creates a Layer 2 builder.
Layer 2 forwards every operation to Layer 1 after resolving identifiers. The split means Layer 1 guarantees valid SQL structure independently, while Layer 2 adds type safety on top.
Join Tracking
When you call .join::<Order>(), the query builder needs to
remember that Order is now part of the query — so it can validate
subsequent operations like .r#where(Order::STATUS, ...) or
.select_as::<Order, _>().
Fabrique tracks this at the type level using a heterogeneous list
(HList): a recursive cons-list where each element can be a different
type. Joining Order to a Product query produces a type like
Joined<Order, Joined<Product, ()>>.
This structure generates heavy type signatures internally. The
Contains trait abstracts over the list to answer one question: “is
model M present in this query?” When it isn’t, the compiler
produces a clear diagnostic:
error: model `Order` is not joined in this query
--> src/main.rs:12:5
|
12 | .r#where(Order::STATUS, "=", "shipped".to_string())
| ^^^^ this column requires its model to be joined
|
= note: add `.join::<Order>()` before using columns
from `Order`
In practice, these type signatures rarely surface. The query builder
is consumed on execution — every method takes self by value, and
the builder is not Clone. As long as you build and execute a query
in a single chain (which is the natural usage), the full type lives
only inside the chain and never appears in your code.
The Typestate Pattern
Each method on the query builder takes self by value and returns a
builder in a new state type. Valid transitions compile; invalid ones
don’t — you cannot diverge from the logical SQL ordering.
The SELECT flow illustrates this:
query ─→ join ─→ where ─→ order_by ─→ limit ─→ offset
│ │ │ ↺ │ │ │
│ │ ↓ ↓ ↓ ↓
└───────┴──── get / first / first_or_fail ─────┘
Joins come before filters, filters before ordering, ordering before
pagination. Calling .join() after .where(), or .set() on a
SELECT query, won’t compile — the method simply doesn’t exist on
that state. INSERT and UPDATE follow the same principle with their
own state machines. See the
API reference for the complete flows.
Efficiently Reprice a Catalog with Bulk Updates
You have a catalog of products and need to reprice them — apply a discount, adjust margins, or import a supplier price list. Doing this one record at a time means N queries for N products. The query builder lets you do it in one.
Apply a Discount in One Query
Mark all expensive products as discounted. A single UPDATE touches every matching row:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory()
.price_cents(15000)
.in_stock(true)
.create(&pool)
.await?;
// Mark all products over $100 as out of stock
Product::update()
.set(Product::IN_STOCK, false)
.r#where(Product::PRICE_CENTS, ">", 10000)
.execute(&pool)
.await?;
let products = Product::query()
.r#where(Product::PRICE_CENTS, ">", 10000)
.get(&pool)
.await?;
assert!(products.iter().all(|p| !p.in_stock));
Ok(())
}
Chain multiple .set() calls to update several columns at once:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory()
.name("Anvil 3000".to_string())
.price_cents(100)
.in_stock(false)
.create(&pool)
.await?;
Product::update()
.set(Product::PRICE_CENTS, 4999)
.set(Product::IN_STOCK, true)
.r#where(Product::NAME, "=", "Anvil 3000")
.execute(&pool)
.await?;
let product = Product::query()
.r#where(Product::NAME, "=", "Anvil 3000")
.first_or_fail(&pool)
.await?;
assert_eq!(product.price_cents, 4999);
assert!(product.in_stock);
Ok(())
}
Get the Updated Records Back
Add .returning() to retrieve the affected rows without a
separate SELECT:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
Product::factory()
.price_cents(15000)
.in_stock(true)
.create(&pool)
.await?;
Product::factory()
.price_cents(20000)
.in_stock(true)
.create(&pool)
.await?;
let updated: Vec<Product> = Product::update()
.set(Product::IN_STOCK, false)
.r#where(Product::PRICE_CENTS, ">", 10000)
.returning()
.get(&pool)
.await?;
println!("Repriced {} products", updated.len());
assert_eq!(updated.len(), 2);
assert!(updated.iter().all(|p| !p.in_stock));
Ok(())
}
Import a Price List (Upsert)
When importing external data, some products may already exist.
on_conflict().do_update() turns the INSERT into an upsert —
inserting new rows and updating existing ones in a single
statement:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let product = Product::factory()
.name("Old Name".to_string())
.create(&pool)
.await?;
let id = product.id;
// Insert or update if the product already exists
let saved: Product = Product::insert()
.set(Product::ID, id)
.set(Product::NAME, "Anvil 3000")
.set(Product::PRICE_CENTS, 5000)
.set(Product::IN_STOCK, true)
.on_conflict()
.do_update()
.returning()
.first_or_fail(&pool)
.await?;
assert_eq!(saved.name, "Anvil 3000");
assert_eq!(saved.price_cents, 5000);
Ok(())
}
.do_update() updates all non-primary-key columns with the
values from the INSERT — the SQL equivalent of
SET col = EXCLUDED.col for each column.
If you only want to skip duplicates without updating, use
.do_nothing():
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Debug, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let id = Uuid::new_v4();
// Insert if not exists, silently skip if exists
Product::insert()
.set(Product::ID, id)
.set(Product::NAME, "Anvil 3000")
.set(Product::PRICE_CENTS, 4999)
.set(Product::IN_STOCK, true)
.on_conflict()
.do_nothing()
.execute(&pool)
.await?;
let found = Product::find(&pool, id).await?;
assert_eq!(found.name, "Anvil 3000");
Ok(())
}
Keep Order History Intact with Soft Deletes
A product is discontinued, but existing orders reference it. A hard DELETE would either violate foreign key constraints or cascade-delete order history. Soft deletes solve this: the record stays in the database with a timestamp marking when it was archived.
For the full list of model methods, see Models > Convenience Methods.
Enable Soft Deletes on a Model
Add a nullable datetime field annotated with
#[fabrique(soft_delete)]:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
extern crate chrono;
use fabrique::prelude::*;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Factory, Model)]
pub struct Product {
id: Uuid,
name: String,
price_cents: i32,
in_stock: bool,
#[fabrique(soft_delete)]
deleted_at: Option<DateTime<Utc>>,
}
fn main() {}
This single annotation changes the behavior of .delete() and
.destroy() — they now set deleted_at instead of issuing a
DELETE statement.
Archive a Product
Call .delete() on the instance. The row — and all its order
line references — stays intact:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
extern crate chrono;
use fabrique::prelude::*;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
#[fabrique(soft_delete)]
pub deleted_at: Option<DateTime<Utc>>,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let anvil = Product::factory()
.name("Anvil 3000".to_string())
.create(&pool)
.await?;
let id = anvil.id;
// Archive it — sets deleted_at to now()
anvil.delete(&pool).await?;
// Convenience methods like all() skip soft-deleted records
let active = Product::all(&pool).await?;
assert!(active.iter().all(|p| p.id != id));
// find() still returns it — useful for admin views
let archived = Product::find(&pool, id).await?;
assert!(archived.trashed(&pool).await?);
Ok(())
}
Note: The query builder does not add hidden WHERE clauses —
Product::query().get()returns all records, including soft-deleted ones. This is by design: implicit filtering would conflict with explicitwhere_not_null/where_nullclauses and add runtime cost. Convenience methods likeall()handle the filtering for you; for custom queries, add.where_null(Product::DELETED_AT)explicitly.
Check and Restore
The supplier restocks the Anvil 3000. Use .restore() to
clear deleted_at and bring the product back:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
extern crate chrono;
use fabrique::prelude::*;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
#[fabrique(soft_delete)]
pub deleted_at: Option<DateTime<Utc>>,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let product = Product::factory().create(&pool).await?;
let id = product.id;
product.delete(&pool).await?;
let archived = Product::find(&pool, id).await?;
archived.restore(&pool).await?;
// Back in active results
let product = Product::find(&pool, id).await?;
assert!(!product.trashed(&pool).await?);
Ok(())
}
Archive by ID Without Fetching
In a REST endpoint you typically have the ID but not the full
model. Product::destroy() soft-deletes by primary key
without a prior SELECT:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
extern crate chrono;
use fabrique::prelude::*;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
#[fabrique(soft_delete)]
pub deleted_at: Option<DateTime<Utc>>,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let product = Product::factory().create(&pool).await?;
let id = product.id;
// DELETE /products/:id handler
Product::destroy(&pool, id).await?;
let product = Product::find(&pool, id).await?;
assert!(product.trashed(&pool).await?);
Ok(())
}
Permanently Remove When Needed
For GDPR compliance or data purges, hard_delete() issues a
real DELETE — bypassing the soft delete mechanism:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
extern crate chrono;
use fabrique::prelude::*;
use uuid::Uuid;
use chrono::{DateTime, Utc};
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
#[fabrique(soft_delete)]
pub deleted_at: Option<DateTime<Utc>>,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let product = Product::factory().create(&pool).await?;
let id = product.id;
// Permanently remove the record
product.hard_delete(&pool).await?;
// Or by ID, without fetching first:
// Product::hard_destroy(&pool, id).await?;
Ok(())
}
Simplify Complex Test Setup with Factories
Testing a feature that spans users, orders, order lines, and products requires creating several related records. Without factories, each test starts with a wall of manual inserts. Factories let you express the same setup in a few chained calls.
For the full factory API, see
Factories. For writing tests with
#[fabrique::test], see
Testing with Fabrique.
Seed a Full Order in One Chain
An order needs a user, products, and the join records linking them. With factories, the entire graph is built in one chain — missing parents are auto-created:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Factory, Model)]
pub struct Product {
pub id: Uuid,
pub name: String,
pub price_cents: i32,
pub in_stock: bool,
}
#[derive(Clone, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
#[fabrique(through = "OrderLine")]
pub products: HasMany<Product>,
}
#[derive(Clone, Factory, Model)]
#[fabrique(table = "order_lines")]
pub struct OrderLine {
#[fabrique(primary_key, belongs_to = "Order")]
pub order_id: Uuid,
#[fabrique(primary_key, belongs_to = "Product")]
pub product_id: Uuid,
pub quantity: i32,
pub unit_price_cents: i32,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let order = Order::factory()
.has_products(Product::factory(), 3)
.create(&pool)
.await?;
Ok(())
}
This single chain creates 1 user, 1 order, 3 products, and 3 order lines — 8 records total. The factory resolves the foreign keys automatically.
Share a Parent Across Children
Three orders for the same customer. Pass a factory instance
to each for_user — the first .create() persists it, the
next ones reuse the same record:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let user = User::factory()
.name("Wile E. Coyote".to_string())
.create(&pool)
.await?;
let pending = Order::factory()
.for_user(user.clone())
.status("pending".to_string())
.create(&pool)
.await?;
let shipped = Order::factory()
.for_user(user.clone())
.status("shipped".to_string())
.create(&pool)
.await?;
assert_eq!(pending.user_id, shipped.user_id);
Ok(())
}
for_user accepts both a model instance and a factory. When
you pass an already-created instance, no extra INSERT happens.
Override Only What Matters
Testing a status filter? Only set status — let the factory
generate everything else:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
pub orders: HasMany<Order>,
}
#[derive(Clone, Factory, Model)]
pub struct Order {
pub id: Uuid,
#[fabrique(belongs_to = "User")]
pub user_id: Uuid,
pub status: String,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Arrange — only the status matters for this test
Order::factory()
.status("shipped".to_string())
.create(&pool)
.await?;
Order::factory()
.status("pending".to_string())
.create(&pool)
.await?;
// Act
let pending: Vec<Order> = Order::query()
.r#where(Order::STATUS, "=", "pending")
.get(&pool)
.await?;
// Assert
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].status, "pending");
Ok(())
}
The test reads at a glance: two orders with different statuses, query for one. No noise from IDs, names, or emails.
Handle Ambiguous Relations
When a model has multiple foreign keys to the same parent
(e.g. Message with sender_id and recipient_id), use
aliases to disambiguate. Fabrique then generates a dedicated
for_<alias>() method for each relationship.
Here, Alice and Bob each send 100 messages to each other — 200 messages total, seeded in a loop:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
}
#[derive(Clone, Factory, Model)]
pub struct Message {
pub id: Uuid,
pub content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
pub sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
pub recipient_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let alice = User::factory()
.name("Alice".to_string())
.create(&pool)
.await?;
let bob = User::factory()
.name("Bob".to_string())
.create(&pool)
.await?;
// Alice sends 100 messages to Bob
for _ in 0..100 {
Message::factory()
.for_sender(alice.clone())
.for_recipient(bob.clone())
.create(&pool)
.await?;
}
// Bob sends 100 messages to Alice
for _ in 0..100 {
Message::factory()
.for_sender(bob.clone())
.for_recipient(alice.clone())
.create(&pool)
.await?;
}
Ok(())
}
For more details on aliases, see Handle Ambiguous Relations.
Handling Multiple belongs_to Relationships
When a model references the same parent model multiple times, you need to disambiguate the relationships using aliases. This guide shows how to set this up correctly.
Define the Child Model with Aliases
Use the alias attribute to give each relationship a unique name:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User { id: Uuid, name: String, email: String }
#[derive(Factory, Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
fn main() {}
This generates:
struct Senderandstruct Recipientas alias marker typesimpl BelongsTo<User, Sender> for Messageimpl BelongsTo<User, Recipient> for Messageimpl Joinable<User, Sender> for Message(and reverse)impl Joinable<User, Recipient> for Message(and reverse)
Define the Parent Model with Aliases
On the parent model, each HasMany relationship must specify which alias it
references:
extern crate fabrique;
extern crate sqlx;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Factory, Model)]
pub struct Message {
id: Uuid,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
#[derive(Clone, Factory, Model)]
pub struct User {
id: Uuid,
name: String,
#[fabrique(alias = "Sender")]
sent_messages: HasMany<Message>,
#[fabrique(alias = "Recipient")]
received_messages: HasMany<Message>,
}
fn main() {}
Named Joins
With aliases, you can join the same model multiple times using join_as:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User {
id: Uuid,
name: String,
email: String
}
#[derive(Factory, Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let messages = Message::query()
.join_as::<User, Sender>()
.join_as::<User, Recipient>()
.get(&pool)
.await?;
Ok(())
}
This generates:
SELECT messages.*
FROM messages
JOIN users AS sender ON sender.id = messages.sender_id
JOIN users AS recipient ON recipient.id = messages.recipient_id
Filtering on Named Joins
Use where_on to filter on a specific alias:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User { id: Uuid, name: String, email: String }
#[derive(Factory, Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Find messages sent by Alice
let messages = Message::query()
.join_as::<User, Sender>()
.where_on::<Sender, _, _, _, _>(User::NAME, "=", "Alice".to_string())
.get(&pool)
.await?;
Ok(())
}
Ordering by Named Joins
Use order_by_on to sort by a column from a specific alias:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User { id: Uuid, name: String, email: String }
#[derive(Factory, Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
// Order messages by sender name
let messages = Message::query()
.join_as::<User, Sender>()
.order_by_on::<Sender, _, _, _>(User::NAME, "ASC")
.get(&pool)
.await?;
Ok(())
}
Using Factories
Aliases generate for_<alias> methods on the factory:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Clone, Factory, Model)]
pub struct User { id: Uuid, name: String, email: String }
#[derive(Factory, Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let alice = User::factory()
.name("Alice".to_string())
.create(&pool)
.await?;
let bob = User::factory()
.name("Bob".to_string())
.create(&pool)
.await?;
let message = Message::factory()
.content("Hello Bob!".to_string())
.for_sender(alice.clone())
.for_recipient(bob)
.create(&pool)
.await?;
Ok(())
}
Using Lazy Loading
With aliases, lazy loading methods work as expected:
extern crate fabrique;
extern crate sqlx;
extern crate tokio;
extern crate uuid;
use fabrique::prelude::*;
use uuid::Uuid;
#[derive(Factory, Model)]
pub struct Message {
id: Uuid,
content: String,
#[fabrique(belongs_to = "User", alias = "Sender")]
sender_id: Uuid,
#[fabrique(belongs_to = "User", alias = "Recipient")]
recipient_id: Uuid,
}
#[derive(Clone, Factory, Model)]
pub struct User {
id: Uuid,
name: String,
email: String,
#[fabrique(alias = "Sender")]
sent_messages: HasMany<Message>,
#[fabrique(alias = "Recipient")]
received_messages: HasMany<Message>,
}
#[fabrique::doctest]
async fn main(pool: Pool<Backend>) -> Result<(), fabrique::Error> {
let user = User::factory().create(&pool).await?;
// Get messages sent by this user
let sent = user.sent_messages().get(&pool).await?;
// Get messages received by this user
let received = user.received_messages().get(&pool).await?;
Ok(())
}