An open API service indexing awesome lists of open source software.

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

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.