| Crates.io | squeal |
| lib.rs | squeal |
| version | 0.1.1 |
| created_at | 2023-12-31 07:44:30.136208+00 |
| updated_at | 2025-12-08 04:39:28.634162+00 |
| description | A SQL query builder library for Rust |
| homepage | |
| repository | https://github.com/upside-down-research/squeal |
| max_upload_size | |
| id | 1084624 |
| size | 222,892 |
A type-safe SQL query builder for Rust targeting PostgreSQL.
squeal provides a simple, type-safe way to construct SQL queries using Rust structures. It offers both direct struct construction and fluent builder APIs with escape hatches built in for complex use cases.
SELECT queries with WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSETJOIN operations (INNER, LEFT, RIGHT, FULL, CROSS) with table or subquery sourcesWITH / Common Table Expressions (CTEs) for complex queriesINSERT statements with single or multiple value sets, or from SELECT queriesINSERT ... ON CONFLICT (UPSERT) with DO NOTHING or DO UPDATEUPDATE statements with SET and WHERE clausesDELETE statements with WHERE conditionsRETURNING clauses for INSERT, UPDATE, and DELETECREATE TABLE DDL statementsDROP TABLE DDL statementsAdd to your Cargo.toml:
[dependencies]
squeal = "0.1.1"
MSRV: Rust 1.90.0
use squeal::*;
// SELECT query with fluent builder
let query = Q()
.select(vec!["id", "name", "email"])
.from("users")
.where_(eq("active", "true"))
.order_by(vec![OrderedColumn::Desc("created_at")])
.limit(10)
.build();
assert_eq!(
query.sql(),
"SELECT id, name, email FROM users WHERE active = true ORDER BY created_at DESC LIMIT 10"
);
// INSERT with fluent builder
let insert = I("users")
.columns(vec!["name", "email"])
.values(vec!["'Alice'", "'alice@example.com'"])
.build();
assert_eq!(
insert.sql(),
"INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')"
);
// UPDATE with fluent builder
let update = U("users")
.columns(vec!["status"])
.values(vec!["'inactive'"])
.where_(lt("last_login", "'2024-01-01'"))
.build();
assert_eq!(
update.sql(),
"UPDATE users SET status = 'inactive' WHERE last_login < '2024-01-01'"
);
// DELETE with fluent builder
let delete = D("logs")
.where_(lt("created_at", "NOW() - INTERVAL '30 days'"))
.build();
For maximum control, you can construct query structs directly:
use squeal::*;
let query = Query {
with_clause: None,
select: Some(Select::new(Columns::Star, None)),
from: Some(FromSource::Table("products")),
joins: vec![],
where_clause: None,
group_by: None,
having: None,
order_by: Some(vec![OrderedColumn::Asc("price")]),
limit: Some(100),
offset: Some(0),
for_update: false,
};
assert_eq!(query.sql(), "SELECT * FROM products ORDER BY price ASC LIMIT 100 OFFSET 0");
Use custom operators and raw SQL fragments when needed:
use squeal::*;
// Use Op::O for custom PostgreSQL operators
let custom_op = Term::Condition(
Box::new(Term::Atom("data")),
Op::O("@>"), // PostgreSQL JSONB contains operator
Box::new(Term::Atom("'{\"type\": \"click\"}'"))
);
let query = Q()
.select(vec!["*"])
.from("events")
.where_(custom_op)
.build();
Combine data from multiple tables with type-safe JOIN operations:
use squeal::*;
// Find all users with their order count
let query = Q()
.select(vec!["users.name", "users.email", "COUNT(orders.id) as order_count"])
.from("users")
.inner_join("orders", eq("users.id", "orders.user_id"))
.group_by(vec!["users.id", "users.name", "users.email"])
.order_by(vec![OrderedColumn::Desc("order_count")])
.build();
// LEFT JOIN to include users with no orders
let query_with_nulls = Q()
.select(vec!["users.name", "COALESCE(orders.total, 0) as total"])
.from("users")
.left_join("orders", eq("users.id", "orders.user_id"))
.build();
Build complex queries with CTEs for better readability:
use squeal::*;
// Calculate monthly revenue and compare to average
let monthly_revenue = Q()
.select(vec![
"DATE_TRUNC('month', created_at) as month",
"SUM(total) as revenue"
])
.from("orders")
.group_by(vec!["month"])
.build();
let query = Q()
.with("monthly_revenue", monthly_revenue)
.select(vec![
"month",
"revenue",
"(revenue - AVG(revenue) OVER ()) as diff_from_avg"
])
.from("monthly_revenue")
.order_by(vec![OrderedColumn::Desc("month")])
.build();
// Result: WITH monthly_revenue AS (SELECT ...) SELECT month, revenue, ...
Handle unique constraint violations gracefully:
use squeal::*;
// Insert user, do nothing if email already exists
let insert = I("users")
.columns(vec!["email", "name", "created_at"])
.values(vec!["'alice@example.com'", "'Alice'", "NOW()"])
.on_conflict_do_nothing(vec!["email"])
.build();
// Result: INSERT INTO users (email, name, created_at) VALUES (...)
// ON CONFLICT (email) DO NOTHING
// Insert or update: update the name if email exists
let upsert = I("users")
.columns(vec!["email", "name", "login_count"])
.values(vec!["'bob@example.com'", "'Bob Smith'", "'1'"])
.on_conflict_do_update(
vec!["email"],
vec![
("name", "'Bob Smith'"),
("login_count", "users.login_count + 1"),
("updated_at", "NOW()")
]
)
.returning(Columns::Selected(vec!["id", "email", "updated_at"]))
.build();
// Result: INSERT INTO users (...) VALUES (...)
// ON CONFLICT (email) DO UPDATE SET name = '...', login_count = ...
// RETURNING id, email, updated_at
Efficiently insert multiple rows in a single statement:
use squeal::*;
let insert = I("products")
.columns(vec!["name", "price", "category"])
.rows(vec![
vec!["'Laptop'", "'999.99'", "'electronics'"],
vec!["'Mouse'", "'24.99'", "'electronics'"],
vec!["'Desk'", "'299.99'", "'furniture'"],
])
.returning(Columns::Selected(vec!["id", "name"]))
.build();
// Result: INSERT INTO products (name, price, category) VALUES
// ('Laptop', 999.99, 'electronics'),
// ('Mouse', 24.99, 'electronics'),
// ('Desk', 299.99, 'furniture')
// RETURNING id, name
Copy data from one table to another:
use squeal::*;
// Archive old orders
let select_old_orders = Q()
.select(vec!["id", "user_id", "total", "created_at"])
.from("orders")
.where_(lt("created_at", "'2023-01-01'"))
.build();
let archive = I("orders_archive")
.columns(vec!["order_id", "user_id", "total", "order_date"])
.select(select_old_orders)
.build();
// Result: INSERT INTO orders_archive (order_id, user_id, total, order_date)
// SELECT id, user_id, total, created_at FROM orders
// WHERE created_at < '2023-01-01'
Get values from INSERT, UPDATE, or DELETE operations:
use squeal::*;
// Get the auto-generated ID after insert
let insert = I("posts")
.columns(vec!["title", "content", "author_id"])
.values(vec!["'Hello World'", "'First post!'", "'1'"])
.returning(Columns::Selected(vec!["id", "created_at"]))
.build();
// Update and return the modified rows
let update = U("users")
.columns(vec!["status", "updated_at"])
.values(vec!["'inactive'", "NOW()"])
.where_(lt("last_login", "NOW() - INTERVAL '1 year'"))
.returning(Columns::Selected(vec!["id", "email"]))
.build();
// Delete and track what was removed
let delete = D("sessions")
.where_(lt("expires_at", "NOW()"))
.returning(Columns::Selected(vec!["user_id", "session_id"]))
.build();
# Build the project
cargo build
# Run tests (excludes Docker tests)
cargo test
# Run all tests including PostgreSQL integration tests (requires Docker)
cargo test --features postgres-docker
# Run benchmarks
cargo bench
# Generate documentation
cargo doc --open
# Run clippy (required before committing)
cargo clippy
# Auto-fix clippy warnings
cargo clippy --fix --lib -p squeal
Pre-release
This library is in active development. The API is stabilizing but may still change. It is suitable for experimentation and early adoption, but not recommended for production use until version 1.0.
A core problem of using SQL in programming is the impedence mismatch between the code of the database and the code of the client.
Efforts such as ActiveRecord exist; they are widely panned by SQL
experts as producing poor SQL and tend not to "fit" the database
itself. Writing SQL directly in code has also a poor history - it is
notorious for defects and difficulty in maintaining.
Query builders occupy a middle point, where the power of the programming language brings to bear some maintainability, but the intermediate artifact is still recognizable and controllable by the programmer, leading to a fit with the database.
This is my contribution in this effort; I developed a Scala query builder for a company quite a few years ago now and it worked well. I am pleased to offer a ground-up cleanroom Rust query builder.
The core problem this design does not touch is the object-relation mapping code. I would advise users to write a database model layer, perform the object-relation map there, then transform into the usual in-system state from that point out.
LGPL 3.0 or later. See LICENSE.md for details.
Contributions are welcome! Please ensure:
All tests pass (cargo test)
No clippy warnings (cargo clippy)
New features include tests
No decrease in code coverage (tarpaulin).
Code follows the existing style
Idealy no dependencies are taken outside of dev-dependencies.
No unsafe code.
This library was hand written, and later AI was asked to extend it. A human contributing _takes responsibility_for their code, whether or not an AI was involved.