Query Builder
Query Builder
Blixt includes a query builder for dynamic CRUD operations. It handles parameterized placeholders for both Postgres ($1, $2) and SQLite (?) automatically based on your feature flag.
All builders are available from the prelude:
use blixt::prelude::*;
// Brings in: Select, Insert, Update, Delete, Order, Value
Select
Basic query
let posts = Select::from("posts")
.columns(&["id", "title", "published"])
.fetch_all::<Post>(&pool)
.await?;
Omitting .columns() selects *.
Filtering
Chain .where_*() methods to add conditions. Multiple conditions are joined with AND.
let posts = Select::from("posts")
.columns(&["id", "title"])
.where_eq("published", true)
.where_gt("score", 10i64)
.fetch_all::<Post>(&pool)
.await?;
Available filter methods:
| Method | SQL operator |
|---|---|
.where_eq(col, val) | = |
.where_ne(col, val) | != |
.where_gt(col, val) | > |
.where_lt(col, val) | < |
.where_gte(col, val) | >= |
.where_lte(col, val) | <= |
Values are automatically converted via the Value enum. Supported types: &str, String, i32, i64, f32, f64, and bool.
Ordering, limit, offset
let recent = Select::from("posts")
.columns(&["id", "title"])
.where_eq("published", true)
.order_by("created_at", Order::Desc)
.limit(10)
.offset(20)
.fetch_all::<Post>(&pool)
.await?;
Fetch methods
| Method | Returns | On no rows |
|---|---|---|
.fetch_all::<T>(pool) | Vec<T> | Empty vec |
.fetch_one::<T>(pool) | T | Error::NotFound (HTTP 404) |
.fetch_optional::<T>(pool) | Option<T> | None |
The target type T must implement sqlx::FromRow.
Insert
With RETURNING
let post = Insert::into("posts")
.set("title", "Hello world")
.set("published", false)
.returning::<Post>(&["id", "title", "published", "created_at", "updated_at"])
.execute(&pool)
.await?;
.returning() adds a RETURNING clause and deserializes the inserted row into T.
Without RETURNING
Insert::into("posts")
.set("title", "Hello world")
.set("published", false)
.execute_no_return(&pool)
.await?;
Update
With RETURNING
let post = Update::table("posts")
.set("title", "Updated title")
.set_timestamp("updated_at")
.where_eq("id", 1i64)
.returning::<Post>(&["id", "title", "published", "created_at", "updated_at"])
.execute(&pool)
.await?;
.set_timestamp(col) sets a column to CURRENT_TIMESTAMP without binding a value. Use it for updated_at columns.
Without RETURNING
Update::table("posts")
.set("score", 0i64)
.where_gt("score", 10i64)
.execute_no_return(&pool)
.await?;
Update supports the same .where_*() methods as Select.
Delete
Delete::from("posts")
.where_eq("id", post_id)
.execute(&pool)
.await?;
A DELETE without any .where_*() conditions logs a warning, since deleting all rows is usually unintentional.
Delete supports the same .where_*() methods as Select.
Builder vs query! macros
Use the query builder when:
- The query structure varies at runtime (optional filters, dynamic sorting)
- You want CRUD operations without writing raw SQL
- You are building model methods (
find_by_id,create,update,delete)
Use query! macros when:
- The query is a fixed string known at compile time
- You need joins, subqueries, aggregations, or other complex SQL
- You want SQLx compile-time verification against your database schema
Both approaches use parameterized binding -- neither is vulnerable to SQL injection.
Complete model example
A model with all five standard CRUD methods using the query builder:
use blixt::prelude::*;
use sqlx::types::chrono::{DateTime, Utc};
const TABLE: &str = "posts";
const COLUMNS: &[&str] = &["id", "title", "published", "created_at", "updated_at"];
#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct Post {
pub id: i64,
pub title: String,
pub published: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Post {
pub async fn find_by_id(pool: &DbPool, id: i64) -> Result<Self> {
Select::from(TABLE).columns(COLUMNS).where_eq("id", id)
.fetch_one::<Self>(pool).await
}
pub async fn find_all(pool: &DbPool) -> Result<Vec<Self>> {
Select::from(TABLE).columns(COLUMNS).order_by("id", Order::Desc)
.fetch_all::<Self>(pool).await
}
pub async fn create(pool: &DbPool, title: &str, published: bool) -> Result<Self> {
Insert::into(TABLE)
.set("title", title)
.set("published", published)
.returning::<Self>(COLUMNS)
.execute(pool).await
}
pub async fn update(pool: &DbPool, id: i64, title: &str, published: bool) -> Result<Self> {
Update::table(TABLE)
.set("title", title)
.set("published", published)
.set_timestamp("updated_at")
.where_eq("id", id)
.returning::<Self>(COLUMNS)
.execute(pool).await
}
pub async fn delete(pool: &DbPool, id: i64) -> Result<()> {
Delete::from(TABLE).where_eq("id", id).execute(pool).await
}
}
This is the same pattern that blixt generate model produces. The TABLE and COLUMNS constants keep column lists in sync between methods. find_by_id returns Error::NotFound (HTTP 404) when no row matches, while find_all returns an empty vec.