https://github.com/walf443/qbey
sql query builder
https://github.com/walf443/qbey
query-builder rust sql
Last synced: 3 months ago
JSON representation
sql query builder
- Host: GitHub
- URL: https://github.com/walf443/qbey
- Owner: walf443
- License: mit
- Created: 2026-03-12T13:58:39.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-23T11:46:37.000Z (3 months ago)
- Last Synced: 2026-03-24T09:06:35.789Z (3 months ago)
- Topics: query-builder, rust, sql
- Language: Rust
- Homepage: https://crates.io/crates/qbey
- Size: 720 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# qbey
sql query builder
## SYNOPSIS
### Basic usage
```rust
# use qbey::{qbey_schema, qbey, col, ConditionExpr, SelectQueryBuilder};
use qbey::prelude::*;
qbey_schema!(Employee, "employee", [id, name, age]);
const EMPLOYEE: Employee = Employee::new();
let mut q = qbey(&EMPLOYEE);
q.and_where(EMPLOYEE.name().eq("Alice"));
q.select(&[EMPLOYEE.id(), EMPLOYEE.name()]);
// Standard SQL (default placeholder: ?)
let (sql, binds) = q.into_sql();
assert_eq!(sql, r#"SELECT "employee"."id", "employee"."name" FROM "employee" WHERE "employee"."name" = ?"#);
```
## Features
- **Dynamic query building** — Conditionally add WHERE clauses, JOINs, and other clauses at runtime. No macro DSL — just plain Rust `if` / `match` for composing queries
- **Safety by default** — UPDATE / DELETE without WHERE is a compile error unless you call `and_where()` or explicitly opt in with `allow_without_where()`. LIKE patterns require `LikeExpression` to prevent wildcard injection. Raw SQL must be wrapped in `RawSql` to make injection boundaries explicit
- **SELECT / INSERT / UPDATE / DELETE** — Full CRUD support including JOIN, GROUP BY / HAVING, UNION, subqueries, and RETURNING (feature flag)
- **Driver agnostic** — Works with any database driver. Tested with [sqlx](https://github.com/launchbadge/sqlx) (SQLite, MySQL), [rusqlite](https://github.com/rusqlite/rusqlite), [tokio-postgres](https://github.com/sfackler/rust-postgres), and [postgres](https://github.com/sfackler/rust-postgres)
- **Extensible bind value types** — Use the built-in `Value` enum for quick prototyping, or define your own type with `qbey_with::()` to match your driver's parameter types
- **Dialect support** — Customize placeholder style (`?`, `$1`, ...) and identifier quoting via the `Dialect` trait. MySQL dialect is available as a separate crate:
- [qbey-mysql](https://github.com/walf443/qbey/tree/main/qbey-mysql) — backtick quoting, index hints, STRAIGHT_JOIN
- **Schema macro** — `qbey_schema!` generates typed column accessors for compile-time checked, qualified column references
## Table of Contents
- [Order By](#order-by)
- [Limit / Offset](#limit--offset)
- [WHERE conditions](#where-conditions) — Comparison, IN, LIKE, BETWEEN, Range, or_where, any/all, not, Dynamic
- [Column aliases](#column-aliases)
- [Raw SQL expressions in SELECT](#raw-sql-expressions-in-select)
- [DISTINCT](#distinct)
- [JOIN](#join)
- [Aggregate / GROUP BY](#aggregate--group-by)
- [HAVING](#having)
- [UNION / UNION ALL](#union--union-all)
- [INSERT](#insert)
- [UPDATE](#update)
- [DELETE](#delete)
- [Dialect support](#dialect-support)
- [RETURNING clause](#returning-clause-feature--returning)
- [MySQL dialect](#mysql-dialect)
- [Schema macro](#schema-macro)
## API
### Order By
```rust
# use qbey::{qbey, col, SelectQueryBuilder};
let mut q = qbey("employee");
q.order_by(col("name").asc());
q.order_by(col("age").desc());
q.select(&["id", "name", "age"]);
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\", \"age\" FROM \"employee\" ORDER BY \"name\" ASC, \"age\" DESC");
```
Use `order_by_expr` to sort by a raw SQL expression (e.g., `RAND()`, `FIELD(...)`, `id DESC NULLS FIRST`).
The expression is rendered as-is, so the caller is responsible for including the sort direction if needed.
[`RawSql`] is required to make it explicit that raw SQL is being injected — **never pass user-supplied input**.
```rust
# use qbey::{qbey, col, RawSql, SelectQueryBuilder};
let mut q = qbey("users");
q.order_by_expr(RawSql::new("RAND()"));
q.select(&["id", "name"]);
let (sql, _) = q.to_sql();
assert_eq!(sql, r#"SELECT "id", "name" FROM "users" ORDER BY RAND()"#);
```
Column-based and expression-based ORDER BY can be mixed:
```rust
# use qbey::{qbey, col, RawSql, SelectQueryBuilder};
let mut q = qbey("users");
q.order_by(col("name").asc());
q.order_by_expr(RawSql::new("RAND()"));
q.select(&["id", "name"]);
let (sql, _) = q.to_sql();
assert_eq!(sql, r#"SELECT "id", "name" FROM "users" ORDER BY "name" ASC, RAND()"#);
```
### Limit / Offset
```rust
# use qbey::{qbey, col, SelectQueryBuilder};
let mut q = qbey("employee");
q.limit(10);
q.offset(20);
q.select(&["id", "name"]);
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"employee\" LIMIT 10 OFFSET 20");
```
### WHERE conditions
#### Comparison operators
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("employee");
q.and_where(("name", "Alice")); // tuple shorthand for Eq
q.and_where(col("age").gt(20)); // age > ?
q.and_where(col("age").lte(60)); // age <= ?
q.and_where(col("salary").lt(100000)); // salary < ?
q.and_where(col("level").gte(3)); // level >= ?
q.and_where(col("role").ne("intern")); // role != ?
q.select(&["id", "name"]);
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"employee\" WHERE \"name\" = ? AND \"age\" > ? AND \"age\" <= ? AND \"salary\" < ? AND \"level\" >= ? AND \"role\" != ?");
```
#### IN / NOT IN
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("users");
q.and_where(col("status").included(&["active", "pending"]));
q.select(&["id", "name"]);
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"users\" WHERE \"status\" IN (?, ?)");
```
Empty lists are safely handled as `1 = 0` (IN) / `1 = 1` (NOT IN).
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("users");
q.and_where(col("status").not_included(&["inactive", "banned"]));
q.select(&["id", "name"]);
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"users\" WHERE \"status\" NOT IN (?, ?)");
```
Subqueries are also supported:
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
let mut sub = qbey("orders");
sub.and_where(col("status").eq("cancelled"));
sub.select(&["user_id"]);
let mut q = qbey("users");
q.and_where(col("id").not_included(sub));
q.select(&["id", "name"]);
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"users\" WHERE \"id\" NOT IN (SELECT \"user_id\" FROM \"orders\" WHERE \"status\" = ?)");
```
#### LIKE / NOT LIKE
`LikeExpression` provides safe pattern construction with automatic escaping of `%` and `_` in user input.
```rust
# use qbey::{qbey, col, LikeExpression, ConditionExpr, SelectQueryBuilder};
// contains: %...%
let (sql, _) = qbey("users")
.and_where(col("name").like(LikeExpression::contains("Ali")))
.to_sql();
assert_eq!(sql, r#"SELECT * FROM "users" WHERE "name" LIKE ? ESCAPE '\'"#);
// starts_with: ...%
let (sql, _) = qbey("users")
.and_where(col("name").like(LikeExpression::starts_with("Ali")))
.to_sql();
assert_eq!(sql, r#"SELECT * FROM "users" WHERE "name" LIKE ? ESCAPE '\'"#);
// ends_with: %...
let (sql, _) = qbey("users")
.and_where(col("name").like(LikeExpression::ends_with("ice")))
.to_sql();
assert_eq!(sql, r#"SELECT * FROM "users" WHERE "name" LIKE ? ESCAPE '\'"#);
// NOT LIKE
let (sql, _) = qbey("users")
.and_where(col("name").not_like(LikeExpression::contains("Bob")))
.to_sql();
assert_eq!(sql, r#"SELECT * FROM "users" WHERE "name" NOT LIKE ? ESCAPE '\'"#);
```
Raw strings are not accepted — `LikeExpression` must be used to prevent wildcard injection.
#### BETWEEN / NOT BETWEEN
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("employee");
q.and_where(col("age").between(20, 30));
q.select(&["id", "name"]);
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"employee\" WHERE \"age\" BETWEEN ? AND ?");
// NOT BETWEEN
let mut q = qbey("employee");
q.and_where(col("age").not_between(20, 30));
q.select(&["id", "name"]);
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"employee\" WHERE \"age\" NOT BETWEEN ? AND ?");
```
#### Range conditions
Rust range types are automatically converted to the appropriate SQL conditions.
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
// Inclusive range: BETWEEN
let (sql, _) = qbey("t").and_where(col("age").in_range(20..=30)).to_sql();
assert_eq!(sql, "SELECT * FROM \"t\" WHERE \"age\" BETWEEN ? AND ?");
// Exclusive range: >= AND <
let (sql, _) = qbey("t").and_where(col("age").in_range(20..30)).to_sql();
assert_eq!(sql, "SELECT * FROM \"t\" WHERE \"age\" >= ? AND \"age\" < ?");
// From range: >=
let (sql, _) = qbey("t").and_where(col("age").in_range(20..)).to_sql();
assert_eq!(sql, "SELECT * FROM \"t\" WHERE \"age\" >= ?");
// To range: <
let (sql, _) = qbey("t").and_where(col("age").in_range(..30)).to_sql();
assert_eq!(sql, "SELECT * FROM \"t\" WHERE \"age\" < ?");
// To inclusive range: <=
let (sql, _) = qbey("t").and_where(col("age").in_range(..=30)).to_sql();
assert_eq!(sql, "SELECT * FROM \"t\" WHERE \"age\" <= ?");
```
#### or_where
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
// Simple OR
let mut q = qbey("employee");
q.and_where(("name", "Alice"));
q.or_where(col("role").eq("admin"));
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT * FROM \"employee\" WHERE \"name\" = ? OR \"role\" = ?");
```
#### Grouping conditions with any / all
```rust
# use qbey::{qbey, col, any, all, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("employee");
q.and_where(("name", "Alice"));
q.and_where(any(col("role").eq("admin"), col("role").eq("manager")));
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT * FROM \"employee\" WHERE \"name\" = ? AND (\"role\" = ? OR \"role\" = ?)");
// Combining all + any
let mut q = qbey("employee");
q.and_where(
any(
all(col("role").eq("admin"), col("dept").eq("eng")),
all(col("role").eq("manager"), col("dept").eq("sales")),
)
);
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT * FROM \"employee\" WHERE (\"role\" = ? AND \"dept\" = ?) OR (\"role\" = ? AND \"dept\" = ?)");
```
#### Negating conditions with not
```rust
# use qbey::{qbey, col, not, any, ConditionExpr, SelectQueryBuilder};
// Function style
let mut q = qbey("employee");
q.and_where(("name", "Alice"));
q.and_where(not(col("role").eq("admin")));
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT * FROM \"employee\" WHERE \"name\" = ? AND NOT (\"role\" = ?)");
// Operator style (! operator)
let mut q = qbey("employee");
q.and_where(!col("role").eq("admin"));
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT * FROM \"employee\" WHERE NOT (\"role\" = ?)");
// Combined with any/all
let mut q = qbey("employee");
q.and_where(not(any(col("role").eq("admin"), col("role").eq("manager"))));
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT * FROM \"employee\" WHERE NOT ((\"role\" = ? OR \"role\" = ?))");
```
#### Dynamic query building
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("employee");
let name: Option<&str> = Some("Alice");
let min_age: Option = Some(20);
if let Some(name) = name {
q.and_where(("name", name));
}
if let Some(min_age) = min_age {
q.and_where(col("age").gt(min_age));
}
q.select(&["id", "name"]);
let (sql, binds) = q.into_sql();
```
### Column aliases
```rust
# use qbey::{qbey, col, SelectQueryBuilder};
let mut q = qbey("users");
q.add_select(col("name").as_("user_name"));
let (sql, _) = q.to_sql();
assert_eq!(sql, "SELECT \"name\" AS \"user_name\" FROM \"users\"");
```
### Raw SQL expressions in SELECT
Use `add_select_expr` to include raw SQL expressions (e.g., function calls) in the SELECT list.
The expression is rendered as-is without quoting, so **never pass user-supplied input** to avoid SQL injection.
```rust
# use qbey::{qbey, col, RawSql, SelectQueryBuilder};
let mut q = qbey("users");
q.add_select(col("id"));
q.add_select_expr(RawSql::new("UPPER(\"name\")"), Some("upper_name"));
q.add_select_expr(RawSql::new("COALESCE(\"nickname\", \"name\")"), Some("display_name"));
let (sql, _) = q.to_sql();
assert_eq!(sql, r#"SELECT "id", UPPER("name") AS "upper_name", COALESCE("nickname", "name") AS "display_name" FROM "users""#);
```
### DISTINCT
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("employee");
q.distinct();
q.select(&["department"]);
let (sql, _) = q.to_sql();
assert_eq!(sql, r#"SELECT DISTINCT "department" FROM "employee""#);
```
DISTINCT can be combined with WHERE and other clauses:
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("employee");
q.and_where(col("active").eq(true));
q.distinct();
q.select(&["department", "role"]);
let (sql, binds) = q.to_sql();
assert_eq!(sql, r#"SELECT DISTINCT "department", "role" FROM "employee" WHERE "active" = ?"#);
```
### JOIN
```rust
# use qbey::{qbey, col, table, join, ConditionExpr, SelectQueryBuilder};
// INNER JOIN with ON
let mut q = qbey("users");
q.join("orders", table("users").col("id").eq(col("user_id")));
q.select(&["id", "name"]);
let (sql, _) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"users\" INNER JOIN \"orders\" ON \"users\".\"id\" = \"orders\".\"user_id\"");
// LEFT JOIN
let mut q = qbey("users");
q.left_join("addresses", table("users").col("id").eq(col("user_id")));
q.select(&["id", "name"]);
let (sql, _) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"users\" LEFT JOIN \"addresses\" ON \"users\".\"id\" = \"addresses\".\"user_id\"");
// JOIN with USING
let mut q = qbey("users");
q.join("orders", join::using_col("user_id"));
q.select(&["id", "name"]);
let (sql, _) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"users\" INNER JOIN \"orders\" USING (\"user_id\")");
// Multiple columns USING
let mut q = qbey("users");
q.join("orders", join::using_cols(&["user_id", "tenant_id"]));
q.select(&["id", "name"]);
let (sql, _) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"users\" INNER JOIN \"orders\" USING (\"user_id\", \"tenant_id\")");
```
#### Table aliases and qualified columns
```rust
# use qbey::{qbey, col, table, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("users");
q.as_("u");
q.join(
table("orders").as_("o"),
table("u").col("id").eq(col("user_id")),
);
q.select(&["id"]);
q.add_select(table("o").col("total").as_("order_total"));
let (sql, _) = q.to_sql();
assert_eq!(sql, "SELECT \"id\", \"o\".\"total\" AS \"order_total\" FROM \"users\" AS \"u\" INNER JOIN \"orders\" AS \"o\" ON \"u\".\"id\" = \"o\".\"user_id\"");
```
### Aggregate / GROUP BY
```rust
# use qbey::{qbey, col, count_all, SelectQueryBuilder};
let mut q = qbey("employee");
q.group_by(&["dept"]);
q.select(&["dept"]);
q.add_select(count_all().as_("cnt"));
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"dept\", COUNT(*) AS \"cnt\" FROM \"employee\" GROUP BY \"dept\"");
```
Raw SQL expressions can also be used for aggregate functions not yet covered by the builder API:
```rust
# use qbey::{qbey, col, RawSql, SelectQueryBuilder};
let mut q = qbey("employee");
q.group_by(&["dept"]);
q.select(&["dept"]);
q.add_select_expr(RawSql::new("COUNT(*)"), Some("cnt"));
q.add_select_expr(RawSql::new("SUM(\"salary\")"), Some("total_salary"));
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"dept\", COUNT(*) AS \"cnt\", SUM(\"salary\") AS \"total_salary\" FROM \"employee\" GROUP BY \"dept\"");
```
### HAVING
Aggregate expressions can be used directly in HAVING clauses, which is required for PostgreSQL compatibility (PostgreSQL does not allow SELECT aliases in HAVING):
```rust
# use qbey::{qbey, col, count_all, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("employee");
q.group_by(&["dept"]);
q.select(&["dept"]);
let cnt = count_all().as_("cnt");
q.add_select(cnt.clone());
q.having(cnt.gt(5));
let (sql, binds) = q.to_sql();
assert_eq!(sql, "SELECT \"dept\", COUNT(*) AS \"cnt\" FROM \"employee\" GROUP BY \"dept\" HAVING COUNT(*) > ?");
```
### UNION / UNION ALL
`union()` / `union_all()` returns a new `Query`, so you can use the same `order_by()`, `limit()`, etc. on the result:
```rust
# use qbey::{qbey, col, SelectQueryBuilder};
let mut q1 = qbey("employee");
q1.and_where(("dept", "eng"));
q1.select(&["id", "name"]);
let mut q2 = qbey("employee");
q2.and_where(("dept", "sales"));
q2.select(&["id", "name"]);
let mut uq = q1.union_all(&q2);
uq.order_by(col("name").asc());
uq.limit(10);
let (sql, binds) = uq.to_sql();
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"employee\" WHERE \"dept\" = ? UNION ALL SELECT \"id\", \"name\" FROM \"employee\" WHERE \"dept\" = ? ORDER BY \"name\" ASC LIMIT 10");
```
### INSERT
`Query::into_insert()` converts a SELECT query builder into an INSERT statement builder.
Values are set using `add_value()` with column-value pairs.
Multiple rows can be inserted by calling `add_value()` multiple times.
Column order may differ between calls — values are automatically reordered to match the first call.
`add_col_value_expr()` appends a raw SQL expression (e.g., `NOW()`) to every row:
```rust
# use qbey::{qbey, col, Value, RawSql, InsertQueryBuilder};
let mut ins = qbey("employee").into_insert();
ins.add_value(&[("name", "Alice".into()), ("age", 30.into())]);
ins.add_value(&[("age", 25.into()), ("name", "Bob".into())]);
ins.add_col_value_expr("created_at", RawSql::new("NOW()"));
let (sql, binds) = ins.into_sql();
assert_eq!(sql, r#"INSERT INTO "employee" ("name", "age", "created_at") VALUES (?, ?, NOW()), (?, ?, NOW())"#);
```
#### ToInsertRow trait
Custom structs can implement `ToInsertRow` to be used directly with `add_value()` / `add_values()`:
```rust
# use qbey::{qbey, col, Value, ToInsertRow, InsertQueryBuilder};
struct Employee {
name: String,
age: i32,
}
impl ToInsertRow for Employee {
fn to_insert_row(&self) -> Vec<(&'static str, Value)> {
vec![
("name", self.name.as_str().into()),
("age", self.age.into()),
]
}
}
let employees = vec![
Employee { name: "Alice".to_string(), age: 30 },
Employee { name: "Bob".to_string(), age: 25 },
];
let mut ins = qbey("employee").into_insert();
// add_values() adds multiple rows at once from a slice
ins.add_values(&employees);
let (sql, binds) = ins.into_sql();
assert_eq!(sql, r#"INSERT INTO "employee" ("name", "age") VALUES (?, ?), (?, ?)"#);
```
#### INSERT ... SELECT
INSERT ... SELECT is also supported via `from_select()`:
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder, InsertQueryBuilder};
let mut sub = qbey("old_employee");
sub.and_where(col("active").eq(true));
sub.select(&["name", "age"]);
let mut ins = qbey("employee").into_insert();
ins.from_select(sub);
let (sql, binds) = ins.into_sql();
assert_eq!(sql, r#"INSERT INTO "employee" SELECT "name", "age" FROM "old_employee" WHERE "active" = ?"#);
```
Calling `to_sql()` / `into_sql()` without any `add_value()` or `from_select()` will panic.
When building rows from a dynamic collection, the caller is responsible for ensuring the collection is non-empty.
### UPDATE
`Query::into_update()` converts a SELECT query builder into an UPDATE statement builder.
```rust
# use qbey::{qbey, col, ConditionExpr, UpdateQueryBuilder};
// Basic UPDATE
let mut u = qbey("employee").into_update();
u.set(col("name"), "Alice");
let u = u.and_where(col("id").eq(1));
let (sql, binds) = u.into_sql();
assert_eq!(sql, r#"UPDATE "employee" SET "name" = ? WHERE "id" = ?"#);
```
WHERE conditions can be built first, then converted to UPDATE using `where_set()`:
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder, UpdateQueryBuilder};
let mut q = qbey("employee");
q.and_where(col("id").eq(1));
let mut u = q.into_update();
u.set(col("name"), "Alice");
u.set(col("age"), 31);
let u = u.where_set();
let (sql, binds) = u.to_sql();
assert_eq!(sql, r#"UPDATE "employee" SET "name" = ?, "age" = ? WHERE "id" = ?"#);
```
By default, `to_sql()` / `into_sql()` is not available until you call `and_where()`, `or_where()`, or `allow_without_where()` — this is enforced at compile time. Use `allow_without_where()` to explicitly allow WHERE-less updates:
```rust
# use qbey::{qbey, col, UpdateQueryBuilder};
let mut u = qbey("employee").into_update();
u.set(col("status"), "inactive");
let u = u.allow_without_where();
let (sql, binds) = u.to_sql();
assert_eq!(sql, r#"UPDATE "employee" SET "status" = ?"#);
```
For raw SQL expressions in SET clauses (e.g. incrementing a counter), use `RawSql`:
```rust
# use qbey::{qbey, col, ConditionExpr, RawSql, UpdateQueryBuilder};
let mut u = qbey("employee").into_update();
u.set_expr(RawSql::new(r#""visit_count" = "visit_count" + 1"#));
let u = u.and_where(col("id").eq(1));
let (sql, binds) = u.to_sql();
assert_eq!(sql, r#"UPDATE "employee" SET "visit_count" = "visit_count" + 1 WHERE "id" = ?"#);
```
`RawSql` supports bind parameters via `{}` placeholders. Use `.binds()` to attach values — they are replaced with dialect-specific placeholders (`?` or `$N`) and collected in the correct order:
```rust
# use qbey::{qbey, col, Value, ConditionExpr, RawSql, UpdateQueryBuilder};
let mut u = qbey("employee").into_update();
u.set_expr(RawSql::new(r#""score" = "score" + {}"#).binds(&[10]));
let u = u.and_where(col("id").eq(1));
let (sql, binds) = u.to_sql();
assert_eq!(sql, r#"UPDATE "employee" SET "score" = "score" + ? WHERE "id" = ?"#);
assert_eq!(binds, vec![Value::Int(10), Value::Int(1)]);
```
### DELETE
`Query::into_delete()` converts a SELECT query builder into a DELETE statement builder.
```rust
# use qbey::{qbey, col, ConditionExpr};
// Basic DELETE
let d = qbey("employee").into_delete()
.and_where(col("id").eq(1));
let (sql, binds) = d.into_sql();
assert_eq!(sql, r#"DELETE FROM "employee" WHERE "id" = ?"#);
```
WHERE conditions can be built first, then converted to DELETE using `where_set()`:
```rust
# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};
let mut q = qbey("employee");
q.and_where(col("id").eq(1));
let d = q.into_delete().where_set();
let (sql, binds) = d.into_sql();
assert_eq!(sql, r#"DELETE FROM "employee" WHERE "id" = ?"#);
```
By default, `to_sql()` / `into_sql()` is not available until you call `and_where()`, `or_where()`, or `allow_without_where()` — this is enforced at compile time. Use `allow_without_where()` to explicitly allow WHERE-less deletes:
```rust
# use qbey::{qbey};
let d = qbey("employee").into_delete()
.allow_without_where();
let (sql, binds) = d.to_sql();
assert_eq!(sql, r#"DELETE FROM "employee""#);
```
### Dialect support
Customize placeholder style and identifier quoting via the `Dialect` trait:
```rust
# use qbey::{qbey, col, ConditionExpr, Dialect, SelectQueryBuilder};
use std::borrow::Cow;
struct PgDialect;
impl Dialect for PgDialect {
fn placeholder(&self, index: usize) -> Cow<'static, str> { Cow::Owned(format!("${}", index)) }
}
let mut q = qbey("employee");
q.and_where(col("name").eq("Alice"));
q.select(&["id", "name"]);
let (sql, binds) = q.to_sql_with(&PgDialect);
assert_eq!(sql, r#"SELECT "id", "name" FROM "employee" WHERE "name" = $1"#);
```
### RETURNING clause (feature = "returning")
RETURNING is non-standard SQL supported by PostgreSQL, SQLite, and MariaDB.
Enable via `features = ["returning"]` in `Cargo.toml`.
INSERT, UPDATE, and DELETE all support `.returning()`:
```rust
# #[cfg(feature = "returning")]
# {
# use qbey::{qbey, col, Value, InsertQueryBuilder};
let mut ins = qbey("employee").into_insert();
ins.add_value(&[("name", "Alice".into()), ("age", 30.into())]);
ins.returning(&[col("id"), col("name")]);
let (sql, binds) = ins.to_sql();
assert_eq!(sql, r#"INSERT INTO "employee" ("name", "age") VALUES (?, ?) RETURNING "id", "name""#);
# }
```
```rust
# #[cfg(feature = "returning")]
# {
# use qbey::{qbey, col, ConditionExpr, UpdateQueryBuilder};
let mut u = qbey("employee").into_update();
u.set(col("name"), "Alice");
let mut u = u.and_where(col("id").eq(1));
u.returning(&[col("id"), col("name")]);
let (sql, binds) = u.to_sql();
assert_eq!(sql, r#"UPDATE "employee" SET "name" = ? WHERE "id" = ? RETURNING "id", "name""#);
# }
```
```rust
# #[cfg(feature = "returning")]
# {
# use qbey::{qbey, col, ConditionExpr};
let mut d = qbey("employee").into_delete()
.and_where(col("id").eq(1));
d.returning(&[col("id"), col("name")]);
let (sql, binds) = d.to_sql();
assert_eq!(sql, r#"DELETE FROM "employee" WHERE "id" = ? RETURNING "id", "name""#);
# }
```
## MySQL dialect
See [qbey-mysql](https://github.com/walf443/qbey/tree/main/qbey-mysql) for MySQL-specific features (backtick quoting, index hints, STRAIGHT_JOIN, etc.).
## Schema macro
`qbey_schema!` generates a typed struct for a table, providing column accessor methods that return qualified `Col` references. This avoids repeating string-based column names and enables compile-time checks.
```rust
# use qbey::{qbey_schema, qbey, col, ConditionExpr, SelectQueryBuilder};
qbey_schema!(Users, "users", [id, name, email]);
let u = Users::new();
let mut q = qbey(&u);
q.and_where(u.name().eq("Alice"));
q.select(&u.all_columns());
let (sql, _) = q.to_sql();
assert_eq!(sql, r#"SELECT "users"."id", "users"."name", "users"."email" FROM "users" WHERE "users"."name" = ?"#);
```
Self-joins are supported via `as_()`:
```rust
# use qbey::{qbey_schema, qbey, col, ConditionExpr, SelectQueryBuilder};
qbey_schema!(Users, "users", [id, name, manager_id]);
let u = Users::new();
let m = Users::new().as_("managers");
let mut q = qbey(&u);
q.left_join(
&m,
u.manager_id().eq(m.id()),
);
q.select(&[u.name(), m.name().as_("manager_name")]);
let (sql, _) = q.to_sql();
assert_eq!(sql, r#"SELECT "users"."name", "managers"."name" AS "manager_name" FROM "users" LEFT JOIN "users" AS "managers" ON "users"."manager_id" = "managers"."id""#);
```
# Example
You can see [walf443/isucon#3](https://github.com/walf443/isucon13/pull/3) for the practical example.