A type-safe, async SQL query builder for Rust, inspired by knex.js.
Named after Archibald Query, the inventor of fluff, because your database deserves queries with character.
- π Fluent API: Clean, chainable query builder inspired by knex.js
- β‘ Async-first: Built for
tokiowith async/await throughout - π¦ Transactions: Full transaction support with savepoints and isolation levels
- π Rich Queries: JOINs, aggregations, GROUP BY, HAVING, ORDER BY, DISTINCT
- π Subqueries: IN, NOT IN, EXISTS, NOT EXISTS, and SELECT subqueries
- π― Parameter Binding: Automatic SQL injection prevention
- ποΈ Multi-Database: PostgreSQL support (MySQL, SQLite planned)
Archibald is strongly opinionated about naked updates and deletes. UPDATE and DELETE operations require WHERE clauses at compile time:
// β Won't compile - missing WHERE clause
update("users").set(data).execute(&pool).await?; // Compile error!
// β Won't compile - missing WHERE clause
delete("users").execute(&pool).await?; // Compile error!
// β
Safe - both SET and WHERE required
update("users")
.set(data)
.where_(("id", 1))
.execute(&pool).await?; // β
Compiles!
// β
Explicit mass updates allowed - if you really mean it
update("users")
.set(data)
.where_((1, 1)) // Explicit "update everything" signal
.execute(&pool).await?;Why this matters: Many SQL data disasters come from missing WHERE clauses. Archibald makes it impossible to forget them.
Add to your Cargo.toml:
# For PostgreSQL
[dependencies]
archibald = { version = "0.1", features = ["postgres"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
serde = { version = "1.0", features = ["derive"] }
# For SQLite
[dependencies]
archibald = { version = "0.1", features = ["sqlite"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
serde = { version = "1.0", features = ["derive"] }use archibald::{from, update, delete, insert, op, transaction};
use archibald::executor::postgres::PostgresPool;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: i32,
name: String,
email: String,
age: i32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to database
let pool = PostgresPool::new("postgresql://user:password@localhost/mydb").await?;
// SELECT query
let users: Vec<User> = from("users")
.select(("id", "name", "email", "age"))
.where_(("age", op::GT, 18))
.and_where(("status", "active"))
.limit(10)
.fetch_all(&pool)
.await?;
println!("Found {} users", users.len());
Ok(())
}use archibald::{from, update, delete, insert, op, transaction};
use archibald::executor::sqlite::SqlitePool;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: i32,
name: String,
email: String,
age: i32,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to SQLite database
let pool = SqlitePool::new("sqlite:example.db").await?;
// SELECT query - identical API to PostgreSQL
let users: Vec<User> = from("users")
.select(("id", "name", "email", "age"))
.where_(("age", op::GT, 18))
.and_where(("status", "active"))
.limit(10)
.fetch_all(&pool)
.await?;
println!("Found {} users", users.len());
Ok(())
}let adults = from("users")
.select(("id", "name", "email"))
.where_(("age", op::GTE, 18)) // age >= 18
.and_where(("status", "active")) // status = 'active' (defaults to EQ)
.and_where(("name", "LIKE", "%john%")) // name LIKE '%john%'
.fetch_all(&pool)
.await?;let user_stats = from("users")
.select((
"users.name",
ColumnSelector::count().as_alias("post_count"),
ColumnSelector::avg("posts.rating").as_alias("avg_rating")
))
.inner_join("posts", "users.id", "posts.user_id")
.where_(("users.active", true))
.group_by("users.id, users.name")
.having(("COUNT(*)", op::GT, 5))
.order_by("avg_rating", SortDirection::Desc)
.fetch_all(&pool)
.await?;// WHERE IN subquery
let active_commenters = from("users")
.select(("id", "name"))
.where_in("id",
from("comments")
.select("user_id")
.where_(("created_at", op::GT, "2024-01-01"))
)
.fetch_all(&pool)
.await?;
// EXISTS subquery
let users_with_orders = from("users")
.select(("id", "name"))
.where_exists(
from("orders")
.select("1")
.where_(("orders.user_id", "users.id"))
)
.fetch_all(&pool)
.await?;use std::collections::HashMap;
let mut user_data = HashMap::new();
user_data.insert("name".to_string(), "Alice".into());
user_data.insert("email".to_string(), "[email protected]".into());
user_data.insert("age".to_string(), 25.into());
let affected = insert("users")
.values(user_data)
.execute(&pool)
.await?;
println!("Inserted {} rows", affected);let mut updates = HashMap::new();
updates.insert("email".to_string(), "[email protected]".into());
updates.insert("last_login".to_string(), "2024-01-15".into());
let affected = update("users")
.set(updates)
.where_(("id", 123))
.and_where(("active", true))
.execute(&pool)
.await?;let affected = delete("users")
.where_(("last_login", op::LT, "2020-01-01"))
.or_where(("status", "inactive"))
.execute(&pool)
.await?;Archibald provides full transaction support with automatic commit/rollback:
use archibald::transaction;
// Simple transaction with automatic commit/rollback
let result = transaction(&pool, |txn| async move {
// Insert user
let user_id = insert("users")
.values(user_data)
.execute_tx(txn)
.await? as i32;
// Create associated profile
let mut profile_data = HashMap::new();
profile_data.insert("user_id".to_string(), user_id.into());
profile_data.insert("bio".to_string(), "Hello world!".into());
insert("user_profiles")
.values(profile_data)
.execute_tx(txn)
.await?;
Ok::<i32, Error>(user_id)
}).await?;
println!("Created user with ID: {}", result);let mut txn = pool.begin_transaction().await?;
// Use savepoints for nested transaction logic
txn.savepoint("before_risky_operation").await?;
match risky_operation(&mut txn).await {
Ok(_) => {
txn.release_savepoint("before_risky_operation").await?;
txn.commit().await?;
}
Err(_) => {
txn.rollback_to_savepoint("before_risky_operation").await?;
// Continue with transaction...
txn.rollback().await?;
}
}use archibald::IsolationLevel;
let txn = pool.begin_transaction_with_isolation(IsolationLevel::Serializable).await?;
// ... use transaction
txn.commit().await?;// PostgreSQL full-text search
let documents = from("articles")
.select(("id", "title"))
.where_(("content", Operator::custom("@@"), "search & query"))
.fetch_all(&pool)
.await?;
// PostGIS distance queries
let nearby = from("locations")
.select(("id", "name"))
.where_(("coordinates", Operator::custom("<->"), point))
.limit(10)
.fetch_all(&pool)
.await?;// Build queries without Result handling
let query = from("users")
.where_(("age", "INVALID_OPERATOR", 18)) // Stored, not validated yet
.and_where(("name", "John"));
// Validation happens at SQL generation
match query.to_sql() {
Ok(sql) => println!("SQL: {}", sql),
Err(e) => println!("Invalid query: {}", e), // "Unknown operator 'INVALID_OPERATOR'"
}Archibald provides safety through:
- Automatic parameter binding - All values are parameterized
- Compile Time Where clauses - UPDATE / DELETE statements require where clauses at compile time
- Validated SQL generation - Invalid queries fail at runtime, not in database
// β
Safe - parameters are automatically bound
let users = from("users")
.where_(("name", user_input)) // Automatically parameterized as $1
.and_where(("age", op::GT, min_age)) // Automatically parameterized as $2
.fetch_all(&pool)
.await?;
// β
Safe - generates: SELECT * FROM users WHERE name = $1 AND age > $2
// Parameters: ["some_user_input", 18]| Database | Status | Features |
|---|---|---|
| PostgreSQL | β Full | All features, parameter binding, transactions |
| SQLite | β Full | All features, JSON as TEXT, limited isolation levels |
| MySQL | π Planned | Coming soon |
- API Documentation
- Examples
- Migration from knex.js (coming soon)
- Core query builder (SELECT, INSERT, UPDATE, DELETE)
- JOINs, subqueries, aggregations
- SQL parameter binding & injection prevention
- Transaction support with savepoints
- Deferred validation architecture
- SQLite support
- Schema builder (CREATE TABLE, ALTER TABLE, etc.)
- Migration system
- MySQL support
- Compile-time schema validation
- Query optimization and caching
Licensed under the LICENSE-MIT or http://opensource.org/licenses/MIT