{"id":47608064,"url":"https://github.com/walf443/qbey","last_synced_at":"2026-04-01T19:39:47.632Z","repository":{"id":343969838,"uuid":"1179905921","full_name":"walf443/qbey","owner":"walf443","description":"sql query builder","archived":false,"fork":false,"pushed_at":"2026-03-23T11:46:37.000Z","size":737,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-24T09:06:35.789Z","etag":null,"topics":["query-builder","rust","sql"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/qbey","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/walf443.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-12T13:58:39.000Z","updated_at":"2026-03-23T11:40:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/walf443/qbey","commit_stats":null,"previous_names":["walf443/sqipe","walf443/qbey"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/walf443/qbey","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walf443%2Fqbey","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walf443%2Fqbey/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walf443%2Fqbey/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walf443%2Fqbey/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/walf443","download_url":"https://codeload.github.com/walf443/qbey/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/walf443%2Fqbey/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291174,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["query-builder","rust","sql"],"created_at":"2026-04-01T19:39:46.744Z","updated_at":"2026-04-01T19:39:47.611Z","avatar_url":"https://github.com/walf443.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# qbey\n\nsql query builder\n\n## SYNOPSIS\n\n### Basic usage\n\n```rust\n# use qbey::{qbey_schema, qbey, col, ConditionExpr, SelectQueryBuilder};\nuse qbey::prelude::*;\n\nqbey_schema!(Employee, \"employee\", [id, name, age]);\nconst EMPLOYEE: Employee = Employee::new();\n\nlet mut q = qbey(\u0026EMPLOYEE);\nq.and_where(EMPLOYEE.name().eq(\"Alice\"));\nq.select(\u0026[EMPLOYEE.id(), EMPLOYEE.name()]);\n\n// Standard SQL (default placeholder: ?)\nlet (sql, binds) = q.into_sql();\nassert_eq!(sql, r#\"SELECT \"employee\".\"id\", \"employee\".\"name\" FROM \"employee\" WHERE \"employee\".\"name\" = ?\"#);\n```\n\n## Features\n\n- **Dynamic query building** — Conditionally add WHERE clauses, JOINs, and other clauses at runtime. No macro DSL — just plain Rust `if` / `match` for composing queries\n- **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\n- **SELECT / INSERT / UPDATE / DELETE** — Full CRUD support including JOIN, GROUP BY / HAVING, UNION, subqueries, and RETURNING (feature flag)\n- **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)\n- **Extensible bind value types** — Use the built-in `Value` enum for quick prototyping, or define your own type with `qbey_with::\u003cV\u003e()` to match your driver's parameter types\n- **Dialect support** — Customize placeholder style (`?`, `$1`, ...) and identifier quoting via the `Dialect` trait. MySQL dialect is available as a separate crate:\n  - [qbey-mysql](https://github.com/walf443/qbey/tree/main/qbey-mysql) — backtick quoting, index hints, STRAIGHT_JOIN\n- **Schema macro** — `qbey_schema!` generates typed column accessors for compile-time checked, qualified column references\n\n## Table of Contents\n\n- [Order By](#order-by)\n- [Limit / Offset](#limit--offset)\n- [WHERE conditions](#where-conditions) — Comparison, IN, LIKE, BETWEEN, Range, or_where, any/all, not, Dynamic\n- [Column aliases](#column-aliases)\n- [Raw SQL expressions in SELECT](#raw-sql-expressions-in-select)\n- [DISTINCT](#distinct)\n- [JOIN](#join)\n- [Aggregate / GROUP BY](#aggregate--group-by)\n- [HAVING](#having)\n- [UNION / UNION ALL](#union--union-all)\n- [INSERT](#insert)\n- [UPDATE](#update)\n- [DELETE](#delete)\n- [Dialect support](#dialect-support)\n- [RETURNING clause](#returning-clause-feature--returning)\n- [MySQL dialect](#mysql-dialect)\n- [Schema macro](#schema-macro)\n\n## API\n\n### Order By\n\n```rust\n# use qbey::{qbey, col, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.order_by(col(\"name\").asc());\nq.order_by(col(\"age\").desc());\nq.select(\u0026[\"id\", \"name\", \"age\"]);\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\", \\\"age\\\" FROM \\\"employee\\\" ORDER BY \\\"name\\\" ASC, \\\"age\\\" DESC\");\n```\n\nUse `order_by_expr` to sort by a raw SQL expression (e.g., `RAND()`, `FIELD(...)`, `id DESC NULLS FIRST`).\nThe expression is rendered as-is, so the caller is responsible for including the sort direction if needed.\n[`RawSql`] is required to make it explicit that raw SQL is being injected — **never pass user-supplied input**.\n\n```rust\n# use qbey::{qbey, col, RawSql, SelectQueryBuilder};\nlet mut q = qbey(\"users\");\nq.order_by_expr(RawSql::new(\"RAND()\"));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, r#\"SELECT \"id\", \"name\" FROM \"users\" ORDER BY RAND()\"#);\n```\n\nColumn-based and expression-based ORDER BY can be mixed:\n\n```rust\n# use qbey::{qbey, col, RawSql, SelectQueryBuilder};\nlet mut q = qbey(\"users\");\nq.order_by(col(\"name\").asc());\nq.order_by_expr(RawSql::new(\"RAND()\"));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, r#\"SELECT \"id\", \"name\" FROM \"users\" ORDER BY \"name\" ASC, RAND()\"#);\n```\n\n### Limit / Offset\n\n```rust\n# use qbey::{qbey, col, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.limit(10);\nq.offset(20);\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"employee\\\" LIMIT 10 OFFSET 20\");\n```\n\n### WHERE conditions\n\n#### Comparison operators\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.and_where((\"name\", \"Alice\"));               // tuple shorthand for Eq\nq.and_where(col(\"age\").gt(20));               // age \u003e ?\nq.and_where(col(\"age\").lte(60));              // age \u003c= ?\nq.and_where(col(\"salary\").lt(100000));        // salary \u003c ?\nq.and_where(col(\"level\").gte(3));             // level \u003e= ?\nq.and_where(col(\"role\").ne(\"intern\"));        // role != ?\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"employee\\\" WHERE \\\"name\\\" = ? AND \\\"age\\\" \u003e ? AND \\\"age\\\" \u003c= ? AND \\\"salary\\\" \u003c ? AND \\\"level\\\" \u003e= ? AND \\\"role\\\" != ?\");\n```\n\n#### IN / NOT IN\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"users\");\nq.and_where(col(\"status\").included(\u0026[\"active\", \"pending\"]));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"users\\\" WHERE \\\"status\\\" IN (?, ?)\");\n```\n\nEmpty lists are safely handled as `1 = 0` (IN) / `1 = 1` (NOT IN).\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"users\");\nq.and_where(col(\"status\").not_included(\u0026[\"inactive\", \"banned\"]));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"users\\\" WHERE \\\"status\\\" NOT IN (?, ?)\");\n```\n\nSubqueries are also supported:\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\nlet mut sub = qbey(\"orders\");\nsub.and_where(col(\"status\").eq(\"cancelled\"));\nsub.select(\u0026[\"user_id\"]);\n\nlet mut q = qbey(\"users\");\nq.and_where(col(\"id\").not_included(sub));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"users\\\" WHERE \\\"id\\\" NOT IN (SELECT \\\"user_id\\\" FROM \\\"orders\\\" WHERE \\\"status\\\" = ?)\");\n```\n\n#### LIKE / NOT LIKE\n\n`LikeExpression` provides safe pattern construction with automatic escaping of `%` and `_` in user input.\n\n```rust\n# use qbey::{qbey, col, LikeExpression, ConditionExpr, SelectQueryBuilder};\n// contains: %...%\nlet (sql, _) = qbey(\"users\")\n    .and_where(col(\"name\").like(LikeExpression::contains(\"Ali\")))\n    .to_sql();\nassert_eq!(sql, r#\"SELECT * FROM \"users\" WHERE \"name\" LIKE ? ESCAPE '\\'\"#);\n\n// starts_with: ...%\nlet (sql, _) = qbey(\"users\")\n    .and_where(col(\"name\").like(LikeExpression::starts_with(\"Ali\")))\n    .to_sql();\nassert_eq!(sql, r#\"SELECT * FROM \"users\" WHERE \"name\" LIKE ? ESCAPE '\\'\"#);\n\n// ends_with: %...\nlet (sql, _) = qbey(\"users\")\n    .and_where(col(\"name\").like(LikeExpression::ends_with(\"ice\")))\n    .to_sql();\nassert_eq!(sql, r#\"SELECT * FROM \"users\" WHERE \"name\" LIKE ? ESCAPE '\\'\"#);\n\n// NOT LIKE\nlet (sql, _) = qbey(\"users\")\n    .and_where(col(\"name\").not_like(LikeExpression::contains(\"Bob\")))\n    .to_sql();\nassert_eq!(sql, r#\"SELECT * FROM \"users\" WHERE \"name\" NOT LIKE ? ESCAPE '\\'\"#);\n```\n\nRaw strings are not accepted — `LikeExpression` must be used to prevent wildcard injection.\n\n#### BETWEEN / NOT BETWEEN\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.and_where(col(\"age\").between(20, 30));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"employee\\\" WHERE \\\"age\\\" BETWEEN ? AND ?\");\n\n// NOT BETWEEN\nlet mut q = qbey(\"employee\");\nq.and_where(col(\"age\").not_between(20, 30));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"employee\\\" WHERE \\\"age\\\" NOT BETWEEN ? AND ?\");\n```\n\n#### Range conditions\n\nRust range types are automatically converted to the appropriate SQL conditions.\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\n// Inclusive range: BETWEEN\nlet (sql, _) = qbey(\"t\").and_where(col(\"age\").in_range(20..=30)).to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"t\\\" WHERE \\\"age\\\" BETWEEN ? AND ?\");\n\n// Exclusive range: \u003e= AND \u003c\nlet (sql, _) = qbey(\"t\").and_where(col(\"age\").in_range(20..30)).to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"t\\\" WHERE \\\"age\\\" \u003e= ? AND \\\"age\\\" \u003c ?\");\n\n// From range: \u003e=\nlet (sql, _) = qbey(\"t\").and_where(col(\"age\").in_range(20..)).to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"t\\\" WHERE \\\"age\\\" \u003e= ?\");\n\n// To range: \u003c\nlet (sql, _) = qbey(\"t\").and_where(col(\"age\").in_range(..30)).to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"t\\\" WHERE \\\"age\\\" \u003c ?\");\n\n// To inclusive range: \u003c=\nlet (sql, _) = qbey(\"t\").and_where(col(\"age\").in_range(..=30)).to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"t\\\" WHERE \\\"age\\\" \u003c= ?\");\n```\n\n#### or_where\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\n// Simple OR\nlet mut q = qbey(\"employee\");\nq.and_where((\"name\", \"Alice\"));\nq.or_where(col(\"role\").eq(\"admin\"));\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"employee\\\" WHERE \\\"name\\\" = ? OR \\\"role\\\" = ?\");\n```\n\n#### Grouping conditions with any / all\n\n```rust\n# use qbey::{qbey, col, any, all, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.and_where((\"name\", \"Alice\"));\nq.and_where(any(col(\"role\").eq(\"admin\"), col(\"role\").eq(\"manager\")));\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"employee\\\" WHERE \\\"name\\\" = ? AND (\\\"role\\\" = ? OR \\\"role\\\" = ?)\");\n\n// Combining all + any\nlet mut q = qbey(\"employee\");\nq.and_where(\n    any(\n        all(col(\"role\").eq(\"admin\"), col(\"dept\").eq(\"eng\")),\n        all(col(\"role\").eq(\"manager\"), col(\"dept\").eq(\"sales\")),\n    )\n);\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"employee\\\" WHERE (\\\"role\\\" = ? AND \\\"dept\\\" = ?) OR (\\\"role\\\" = ? AND \\\"dept\\\" = ?)\");\n```\n\n#### Negating conditions with not\n\n```rust\n# use qbey::{qbey, col, not, any, ConditionExpr, SelectQueryBuilder};\n// Function style\nlet mut q = qbey(\"employee\");\nq.and_where((\"name\", \"Alice\"));\nq.and_where(not(col(\"role\").eq(\"admin\")));\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"employee\\\" WHERE \\\"name\\\" = ? AND NOT (\\\"role\\\" = ?)\");\n\n// Operator style (! operator)\nlet mut q = qbey(\"employee\");\nq.and_where(!col(\"role\").eq(\"admin\"));\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"employee\\\" WHERE NOT (\\\"role\\\" = ?)\");\n\n// Combined with any/all\nlet mut q = qbey(\"employee\");\nq.and_where(not(any(col(\"role\").eq(\"admin\"), col(\"role\").eq(\"manager\"))));\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT * FROM \\\"employee\\\" WHERE NOT ((\\\"role\\\" = ? OR \\\"role\\\" = ?))\");\n```\n\n#### Dynamic query building\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\n\nlet name: Option\u003c\u0026str\u003e = Some(\"Alice\");\nlet min_age: Option\u003ci32\u003e = Some(20);\n\nif let Some(name) = name {\n    q.and_where((\"name\", name));\n}\nif let Some(min_age) = min_age {\n    q.and_where(col(\"age\").gt(min_age));\n}\n\nq.select(\u0026[\"id\", \"name\"]);\nlet (sql, binds) = q.into_sql();\n```\n\n### Column aliases\n\n```rust\n# use qbey::{qbey, col, SelectQueryBuilder};\nlet mut q = qbey(\"users\");\nq.add_select(col(\"name\").as_(\"user_name\"));\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"name\\\" AS \\\"user_name\\\" FROM \\\"users\\\"\");\n```\n\n### Raw SQL expressions in SELECT\n\nUse `add_select_expr` to include raw SQL expressions (e.g., function calls) in the SELECT list.\nThe expression is rendered as-is without quoting, so **never pass user-supplied input** to avoid SQL injection.\n\n```rust\n# use qbey::{qbey, col, RawSql, SelectQueryBuilder};\nlet mut q = qbey(\"users\");\nq.add_select(col(\"id\"));\nq.add_select_expr(RawSql::new(\"UPPER(\\\"name\\\")\"), Some(\"upper_name\"));\nq.add_select_expr(RawSql::new(\"COALESCE(\\\"nickname\\\", \\\"name\\\")\"), Some(\"display_name\"));\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, r#\"SELECT \"id\", UPPER(\"name\") AS \"upper_name\", COALESCE(\"nickname\", \"name\") AS \"display_name\" FROM \"users\"\"#);\n```\n\n### DISTINCT\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.distinct();\nq.select(\u0026[\"department\"]);\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, r#\"SELECT DISTINCT \"department\" FROM \"employee\"\"#);\n```\n\nDISTINCT can be combined with WHERE and other clauses:\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.and_where(col(\"active\").eq(true));\nq.distinct();\nq.select(\u0026[\"department\", \"role\"]);\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, r#\"SELECT DISTINCT \"department\", \"role\" FROM \"employee\" WHERE \"active\" = ?\"#);\n```\n\n### JOIN\n\n```rust\n# use qbey::{qbey, col, table, join, ConditionExpr, SelectQueryBuilder};\n// INNER JOIN with ON\nlet mut q = qbey(\"users\");\nq.join(\"orders\", table(\"users\").col(\"id\").eq(col(\"user_id\")));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"users\\\" INNER JOIN \\\"orders\\\" ON \\\"users\\\".\\\"id\\\" = \\\"orders\\\".\\\"user_id\\\"\");\n\n// LEFT JOIN\nlet mut q = qbey(\"users\");\nq.left_join(\"addresses\", table(\"users\").col(\"id\").eq(col(\"user_id\")));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"users\\\" LEFT JOIN \\\"addresses\\\" ON \\\"users\\\".\\\"id\\\" = \\\"addresses\\\".\\\"user_id\\\"\");\n\n// JOIN with USING\nlet mut q = qbey(\"users\");\nq.join(\"orders\", join::using_col(\"user_id\"));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"users\\\" INNER JOIN \\\"orders\\\" USING (\\\"user_id\\\")\");\n\n// Multiple columns USING\nlet mut q = qbey(\"users\");\nq.join(\"orders\", join::using_cols(\u0026[\"user_id\", \"tenant_id\"]));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"users\\\" INNER JOIN \\\"orders\\\" USING (\\\"user_id\\\", \\\"tenant_id\\\")\");\n```\n\n#### Table aliases and qualified columns\n\n```rust\n# use qbey::{qbey, col, table, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"users\");\nq.as_(\"u\");\nq.join(\n    table(\"orders\").as_(\"o\"),\n    table(\"u\").col(\"id\").eq(col(\"user_id\")),\n);\nq.select(\u0026[\"id\"]);\nq.add_select(table(\"o\").col(\"total\").as_(\"order_total\"));\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"o\\\".\\\"total\\\" AS \\\"order_total\\\" FROM \\\"users\\\" AS \\\"u\\\" INNER JOIN \\\"orders\\\" AS \\\"o\\\" ON \\\"u\\\".\\\"id\\\" = \\\"o\\\".\\\"user_id\\\"\");\n```\n\n### Aggregate / GROUP BY\n\n```rust\n# use qbey::{qbey, col, count_all, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.group_by(\u0026[\"dept\"]);\nq.select(\u0026[\"dept\"]);\nq.add_select(count_all().as_(\"cnt\"));\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"dept\\\", COUNT(*) AS \\\"cnt\\\" FROM \\\"employee\\\" GROUP BY \\\"dept\\\"\");\n```\n\nRaw SQL expressions can also be used for aggregate functions not yet covered by the builder API:\n\n```rust\n# use qbey::{qbey, col, RawSql, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.group_by(\u0026[\"dept\"]);\nq.select(\u0026[\"dept\"]);\nq.add_select_expr(RawSql::new(\"COUNT(*)\"), Some(\"cnt\"));\nq.add_select_expr(RawSql::new(\"SUM(\\\"salary\\\")\"), Some(\"total_salary\"));\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"dept\\\", COUNT(*) AS \\\"cnt\\\", SUM(\\\"salary\\\") AS \\\"total_salary\\\" FROM \\\"employee\\\" GROUP BY \\\"dept\\\"\");\n```\n\n### HAVING\n\nAggregate expressions can be used directly in HAVING clauses, which is required for PostgreSQL compatibility (PostgreSQL does not allow SELECT aliases in HAVING):\n\n```rust\n# use qbey::{qbey, col, count_all, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.group_by(\u0026[\"dept\"]);\nq.select(\u0026[\"dept\"]);\nlet cnt = count_all().as_(\"cnt\");\nq.add_select(cnt.clone());\nq.having(cnt.gt(5));\n\nlet (sql, binds) = q.to_sql();\nassert_eq!(sql, \"SELECT \\\"dept\\\", COUNT(*) AS \\\"cnt\\\" FROM \\\"employee\\\" GROUP BY \\\"dept\\\" HAVING COUNT(*) \u003e ?\");\n```\n\n### UNION / UNION ALL\n\n`union()` / `union_all()` returns a new `Query`, so you can use the same `order_by()`, `limit()`, etc. on the result:\n\n```rust\n# use qbey::{qbey, col, SelectQueryBuilder};\nlet mut q1 = qbey(\"employee\");\nq1.and_where((\"dept\", \"eng\"));\nq1.select(\u0026[\"id\", \"name\"]);\n\nlet mut q2 = qbey(\"employee\");\nq2.and_where((\"dept\", \"sales\"));\nq2.select(\u0026[\"id\", \"name\"]);\n\nlet mut uq = q1.union_all(\u0026q2);\nuq.order_by(col(\"name\").asc());\nuq.limit(10);\n\nlet (sql, binds) = uq.to_sql();\nassert_eq!(sql, \"SELECT \\\"id\\\", \\\"name\\\" FROM \\\"employee\\\" WHERE \\\"dept\\\" = ? UNION ALL SELECT \\\"id\\\", \\\"name\\\" FROM \\\"employee\\\" WHERE \\\"dept\\\" = ? ORDER BY \\\"name\\\" ASC LIMIT 10\");\n```\n\n### INSERT\n\n`Query::into_insert()` converts a SELECT query builder into an INSERT statement builder.\nValues are set using `add_value()` with column-value pairs.\nMultiple rows can be inserted by calling `add_value()` multiple times.\nColumn order may differ between calls — values are automatically reordered to match the first call.\n`add_col_value_expr()` appends a raw SQL expression (e.g., `NOW()`) to every row:\n\n```rust\n# use qbey::{qbey, col, Value, RawSql, InsertQueryBuilder};\nlet mut ins = qbey(\"employee\").into_insert();\nins.add_value(\u0026[(\"name\", \"Alice\".into()), (\"age\", 30.into())]);\nins.add_value(\u0026[(\"age\", 25.into()), (\"name\", \"Bob\".into())]);\nins.add_col_value_expr(\"created_at\", RawSql::new(\"NOW()\"));\n\nlet (sql, binds) = ins.into_sql();\nassert_eq!(sql, r#\"INSERT INTO \"employee\" (\"name\", \"age\", \"created_at\") VALUES (?, ?, NOW()), (?, ?, NOW())\"#);\n```\n\n#### ToInsertRow trait\n\nCustom structs can implement `ToInsertRow\u003cV\u003e` to be used directly with `add_value()` / `add_values()`:\n\n```rust\n# use qbey::{qbey, col, Value, ToInsertRow, InsertQueryBuilder};\nstruct Employee {\n    name: String,\n    age: i32,\n}\n\nimpl ToInsertRow\u003cValue\u003e for Employee {\n    fn to_insert_row(\u0026self) -\u003e Vec\u003c(\u0026'static str, Value)\u003e {\n        vec![\n            (\"name\", self.name.as_str().into()),\n            (\"age\", self.age.into()),\n        ]\n    }\n}\n\nlet employees = vec![\n    Employee { name: \"Alice\".to_string(), age: 30 },\n    Employee { name: \"Bob\".to_string(), age: 25 },\n];\n\nlet mut ins = qbey(\"employee\").into_insert();\n// add_values() adds multiple rows at once from a slice\nins.add_values(\u0026employees);\n\nlet (sql, binds) = ins.into_sql();\nassert_eq!(sql, r#\"INSERT INTO \"employee\" (\"name\", \"age\") VALUES (?, ?), (?, ?)\"#);\n```\n\n#### INSERT ... SELECT\n\nINSERT ... SELECT is also supported via `from_select()`:\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder, InsertQueryBuilder};\nlet mut sub = qbey(\"old_employee\");\nsub.and_where(col(\"active\").eq(true));\nsub.select(\u0026[\"name\", \"age\"]);\n\nlet mut ins = qbey(\"employee\").into_insert();\nins.from_select(sub);\n\nlet (sql, binds) = ins.into_sql();\nassert_eq!(sql, r#\"INSERT INTO \"employee\" SELECT \"name\", \"age\" FROM \"old_employee\" WHERE \"active\" = ?\"#);\n```\n\nCalling `to_sql()` / `into_sql()` without any `add_value()` or `from_select()` will panic.\nWhen building rows from a dynamic collection, the caller is responsible for ensuring the collection is non-empty.\n\n### UPDATE\n\n`Query::into_update()` converts a SELECT query builder into an UPDATE statement builder.\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, UpdateQueryBuilder};\n// Basic UPDATE\nlet mut u = qbey(\"employee\").into_update();\nu.set(col(\"name\"), \"Alice\");\nlet u = u.and_where(col(\"id\").eq(1));\n\nlet (sql, binds) = u.into_sql();\nassert_eq!(sql, r#\"UPDATE \"employee\" SET \"name\" = ? WHERE \"id\" = ?\"#);\n```\n\nWHERE conditions can be built first, then converted to UPDATE using `where_set()`:\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder, UpdateQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.and_where(col(\"id\").eq(1));\nlet mut u = q.into_update();\nu.set(col(\"name\"), \"Alice\");\nu.set(col(\"age\"), 31);\n\nlet u = u.where_set();\nlet (sql, binds) = u.to_sql();\nassert_eq!(sql, r#\"UPDATE \"employee\" SET \"name\" = ?, \"age\" = ? WHERE \"id\" = ?\"#);\n```\n\nBy 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:\n\n```rust\n# use qbey::{qbey, col, UpdateQueryBuilder};\nlet mut u = qbey(\"employee\").into_update();\nu.set(col(\"status\"), \"inactive\");\nlet u = u.allow_without_where();\n\nlet (sql, binds) = u.to_sql();\nassert_eq!(sql, r#\"UPDATE \"employee\" SET \"status\" = ?\"#);\n```\n\nFor raw SQL expressions in SET clauses (e.g. incrementing a counter), use `RawSql`:\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, RawSql, UpdateQueryBuilder};\nlet mut u = qbey(\"employee\").into_update();\nu.set_expr(RawSql::new(r#\"\"visit_count\" = \"visit_count\" + 1\"#));\nlet u = u.and_where(col(\"id\").eq(1));\n\nlet (sql, binds) = u.to_sql();\nassert_eq!(sql, r#\"UPDATE \"employee\" SET \"visit_count\" = \"visit_count\" + 1 WHERE \"id\" = ?\"#);\n```\n\n`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:\n\n```rust\n# use qbey::{qbey, col, Value, ConditionExpr, RawSql, UpdateQueryBuilder};\nlet mut u = qbey(\"employee\").into_update();\nu.set_expr(RawSql::new(r#\"\"score\" = \"score\" + {}\"#).binds(\u0026[10]));\nlet u = u.and_where(col(\"id\").eq(1));\n\nlet (sql, binds) = u.to_sql();\nassert_eq!(sql, r#\"UPDATE \"employee\" SET \"score\" = \"score\" + ? WHERE \"id\" = ?\"#);\nassert_eq!(binds, vec![Value::Int(10), Value::Int(1)]);\n```\n\n### DELETE\n\n`Query::into_delete()` converts a SELECT query builder into a DELETE statement builder.\n\n```rust\n# use qbey::{qbey, col, ConditionExpr};\n// Basic DELETE\nlet d = qbey(\"employee\").into_delete()\n    .and_where(col(\"id\").eq(1));\n\nlet (sql, binds) = d.into_sql();\nassert_eq!(sql, r#\"DELETE FROM \"employee\" WHERE \"id\" = ?\"#);\n```\n\nWHERE conditions can be built first, then converted to DELETE using `where_set()`:\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, SelectQueryBuilder};\nlet mut q = qbey(\"employee\");\nq.and_where(col(\"id\").eq(1));\nlet d = q.into_delete().where_set();\n\nlet (sql, binds) = d.into_sql();\nassert_eq!(sql, r#\"DELETE FROM \"employee\" WHERE \"id\" = ?\"#);\n```\n\nBy 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:\n\n```rust\n# use qbey::{qbey};\nlet d = qbey(\"employee\").into_delete()\n    .allow_without_where();\n\nlet (sql, binds) = d.to_sql();\nassert_eq!(sql, r#\"DELETE FROM \"employee\"\"#);\n```\n\n### Dialect support\n\nCustomize placeholder style and identifier quoting via the `Dialect` trait:\n\n```rust\n# use qbey::{qbey, col, ConditionExpr, Dialect, SelectQueryBuilder};\nuse std::borrow::Cow;\nstruct PgDialect;\nimpl Dialect for PgDialect {\n    fn placeholder(\u0026self, index: usize) -\u003e Cow\u003c'static, str\u003e { Cow::Owned(format!(\"${}\", index)) }\n}\n\nlet mut q = qbey(\"employee\");\nq.and_where(col(\"name\").eq(\"Alice\"));\nq.select(\u0026[\"id\", \"name\"]);\n\nlet (sql, binds) = q.to_sql_with(\u0026PgDialect);\nassert_eq!(sql, r#\"SELECT \"id\", \"name\" FROM \"employee\" WHERE \"name\" = $1\"#);\n```\n\n### RETURNING clause (feature = \"returning\")\n\nRETURNING is non-standard SQL supported by PostgreSQL, SQLite, and MariaDB.\nEnable via `features = [\"returning\"]` in `Cargo.toml`.\n\nINSERT, UPDATE, and DELETE all support `.returning()`:\n\n```rust\n# #[cfg(feature = \"returning\")]\n# {\n# use qbey::{qbey, col, Value, InsertQueryBuilder};\nlet mut ins = qbey(\"employee\").into_insert();\nins.add_value(\u0026[(\"name\", \"Alice\".into()), (\"age\", 30.into())]);\nins.returning(\u0026[col(\"id\"), col(\"name\")]);\n\nlet (sql, binds) = ins.to_sql();\nassert_eq!(sql, r#\"INSERT INTO \"employee\" (\"name\", \"age\") VALUES (?, ?) RETURNING \"id\", \"name\"\"#);\n# }\n```\n\n```rust\n# #[cfg(feature = \"returning\")]\n# {\n# use qbey::{qbey, col, ConditionExpr, UpdateQueryBuilder};\nlet mut u = qbey(\"employee\").into_update();\nu.set(col(\"name\"), \"Alice\");\nlet mut u = u.and_where(col(\"id\").eq(1));\nu.returning(\u0026[col(\"id\"), col(\"name\")]);\n\nlet (sql, binds) = u.to_sql();\nassert_eq!(sql, r#\"UPDATE \"employee\" SET \"name\" = ? WHERE \"id\" = ? RETURNING \"id\", \"name\"\"#);\n# }\n```\n\n```rust\n# #[cfg(feature = \"returning\")]\n# {\n# use qbey::{qbey, col, ConditionExpr};\nlet mut d = qbey(\"employee\").into_delete()\n    .and_where(col(\"id\").eq(1));\nd.returning(\u0026[col(\"id\"), col(\"name\")]);\n\nlet (sql, binds) = d.to_sql();\nassert_eq!(sql, r#\"DELETE FROM \"employee\" WHERE \"id\" = ? RETURNING \"id\", \"name\"\"#);\n# }\n```\n\n## MySQL dialect\n\nSee [qbey-mysql](https://github.com/walf443/qbey/tree/main/qbey-mysql) for MySQL-specific features (backtick quoting, index hints, STRAIGHT_JOIN, etc.).\n\n## Schema macro\n\n`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.\n\n```rust\n# use qbey::{qbey_schema, qbey, col, ConditionExpr, SelectQueryBuilder};\nqbey_schema!(Users, \"users\", [id, name, email]);\n\nlet u = Users::new();\nlet mut q = qbey(\u0026u);\nq.and_where(u.name().eq(\"Alice\"));\nq.select(\u0026u.all_columns());\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, r#\"SELECT \"users\".\"id\", \"users\".\"name\", \"users\".\"email\" FROM \"users\" WHERE \"users\".\"name\" = ?\"#);\n```\n\nSelf-joins are supported via `as_()`:\n\n```rust\n# use qbey::{qbey_schema, qbey, col, ConditionExpr, SelectQueryBuilder};\nqbey_schema!(Users, \"users\", [id, name, manager_id]);\n\nlet u = Users::new();\nlet m = Users::new().as_(\"managers\");\nlet mut q = qbey(\u0026u);\nq.left_join(\n    \u0026m,\n    u.manager_id().eq(m.id()),\n);\nq.select(\u0026[u.name(), m.name().as_(\"manager_name\")]);\n\nlet (sql, _) = q.to_sql();\nassert_eq!(sql, r#\"SELECT \"users\".\"name\", \"managers\".\"name\" AS \"manager_name\" FROM \"users\" LEFT JOIN \"users\" AS \"managers\" ON \"users\".\"manager_id\" = \"managers\".\"id\"\"#);\n```\n\n# Example\n\nYou can see [walf443/isucon#3](https://github.com/walf443/isucon13/pull/3) for the practical example.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwalf443%2Fqbey","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwalf443%2Fqbey","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwalf443%2Fqbey/lists"}