Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Product to the products table
  • Uses id as 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:

  1. Start a query with Product::query()
  2. Add a WHERE clause with .r#where()
  3. 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:

  1. Derive Model on your structs to enable database operations
  2. Use the query builder with first_or_fail for primary key lookups
  3. Use the query builder with r#where for filtered queries
  4. Use create and save for persistence
  5. Use destroy for deletion

Next Steps

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 Order and Product
  • 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:

  1. belongs_to marks foreign key fields
  2. HasMany<T> declares one-to-many relationships
  3. through enables many-to-many relationships via join tables
  4. Composite primary keys work naturally with #[fabrique(primary_key)]
  5. Relation methods return query builders for lazy loading

Next Steps

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

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, and price_cents are 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:

ExpressionExample 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:

  1. #[fabrique::test] gives each test an isolated, migrated database
  2. Factories generate test data — set only the fields that matter for your assertion
  3. for_<relation> and has_<relation> set up related records without manual foreign key wiring
  4. faker produces realistic data when random values aren’t enough

Next Steps

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 path
  • Product::insert() — starts an INSERT
  • Product::update() — starts an UPDATE

Why query() and not select()? In SQL, SELECT comes 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() and model.save() (see Models) wrap this query builder internally. They return the persisted record using RETURNING. MySQL doesn’t support RETURNING — 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> for Order — exposes the foreign key column for queries and factories
  • Joinable<User> for Order — enables Order::query().join::<User>()
  • Joinable<Order> for User — enables User::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 Sender and struct Recipient
  • BelongsTo<User, Sender> and BelongsTo<User, Recipient> for Message
  • Bidirectional Joinable impls 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 testing feature 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 explicit where_not_null / where_null clauses and add runtime cost. Convenience methods like all() 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 Sender and struct Recipient as alias marker types
  • impl BelongsTo<User, Sender> for Message
  • impl BelongsTo<User, Recipient> for Message
  • impl 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(())
}