Skip to main content

keelson_psql/
lib.rs

1//! The PostgreSQL dialect for keelson.
2//!
3//! The statement types — `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `MERGE`, and
4//! the `VALUES` and `TABLE` shorthands — each shaped by the production in
5//! PostgreSQL's own reference manual, plus the mods that fill them in and the
6//! expression starters they are filled in with.
7//!
8//! ```
9//! use keelson_psql as psql;
10//! use keelson_psql::{Chain, Query, arg, quote, select};
11//!
12//! let q = psql::select((
13//!     select::columns((quote("id"), quote("name"))),
14//!     select::from(quote("users")),
15//!     select::where_(quote("age").gte(arg(21i32))),
16//! ));
17//!
18//! let (sql, args) = q.build()?;
19//! assert_eq!(sql, r#"SELECT "id", "name" FROM "users" WHERE ("age" >= $1)"#);
20//! assert_eq!(args, vec![keelson_core::Value::I32(21)]);
21//! # Ok::<_, keelson_core::Error>(())
22//! ```
23//!
24//! # Where this sits
25//!
26//! Layer 1 of keelson, for PostgreSQL. It is a complete way to use keelson on
27//! its own: it depends only on [keelson-core](https://docs.rs/keelson-core) and produces a
28//! SQL string and an argument list, which you may run with any driver you like.
29//! To run it through keelson, add Layer 2 ([keelson-exec](https://docs.rs/keelson-exec) plus a
30//! backend such as [keelson-sqlx](https://docs.rs/keelson-sqlx)); to have typed models built
31//! out of these mods, add Layers 3 and 4 ([keelson-models](https://docs.rs/keelson-models),
32//! [keelson-gen](https://docs.rs/keelson-gen)). The whole map, and one dependency line for it,
33//! is the [keelson](https://docs.rs/keelson) facade crate.
34//!
35//! # How it is put together
36//!
37//! **A starter is a function of one mod.** `psql::select(mods)` takes a single
38//! `impl Mod<SelectQuery>` — and a tuple of mods is a mod, so
39//! `psql::select(())` and `psql::select((a, b, c))` are both that one argument.
40//! Arity is never a ceiling, because tuples nest.
41//!
42//! **A mod module shares its name with its starter.** `psql::select` is a function
43//! *and* a module: Rust keeps values and modules in separate namespaces, so
44//! `psql::select((select::from("users"),))` needs no import gymnastics. The modules
45//! are named after the statement — [`select`](mod@select), [`insert`](mod@insert),
46//! [`update`](mod@update), [`delete`](mod@delete),
47//! [`window`], [`frame`] — never bob's `sm`/`im`/`um`/`dm`/`wm`/`fm`.
48//!
49//! **A mod is written once.** The mods live in one place, generic over the
50//! `keelson_core::clause` `Has*` trait they need, and each statement module
51//! re-exports the ones that apply to it. `select::where_` and `update::where_` are
52//! the same function; `insert::where_` is too, and resolves only against the
53//! `ON CONFLICT … DO UPDATE` body, because an `INSERT` has no `WHERE` of its own.
54//! An inapplicable mod is a compile error.
55//!
56//! **A raw `&str` works wherever an expression does.** Every slot takes
57//! `impl IntoExpr`, and a `&'static str` is raw SQL. `select::from("users")` writes
58//! `FROM users`; `select::from(quote("users"))` writes `FROM "users"`.
59//!
60//! # Sub-queries
61//!
62//! The four query types implement [`IntoExpr`], so one goes straight into any
63//! expression slot: `select::union(other)`, `select::with("c", other)`,
64//! `insert::query(other)`. Those slots supply their own parentheses. Where the
65//! parentheses belong to the sub-query itself — a `FROM` item, a scalar
66//! sub-expression — use [`subquery`]. Placeholders re-index across the nesting on
67//! their own, because the counter belongs to the writer.
68
69#![warn(missing_docs)]
70
71mod dialect;
72mod extras;
73mod function;
74mod ops;
75pub mod shared;
76mod statement;
77
78pub mod delete;
79pub mod frame;
80pub mod insert;
81pub mod merge;
82pub mod select;
83pub mod table;
84pub mod update;
85pub mod values;
86pub mod window;
87
88pub use dialect::Psql;
89pub use extras::{Distinct, Overriding, cube, excluded, grouping_sets, query, rollup, subquery};
90pub use function::{ColumnDef, Function, TableFunction};
91pub use ops::PsqlOps;
92pub use statement::{
93    DeleteQuery, HasExtraTables, HasTargetTable, InsertQuery, MergeAction, MergeInsert,
94    MergeMatchKind, MergeQuery, MergeWhen, SelectQuery, TableQuery, UpdateQuery, ValuesQuery,
95};
96
97// The core vocabulary a caller needs in order to use any of the above, re-exported
98// so that a program building PostgreSQL queries needs one dependency and one `use`.
99pub use keelson_core::expr::{CaseBuilder, Chain, Expr, IntoExpr, IntoExprList, IntoIdent, RawArg};
100pub use keelson_core::{Error, Mod, Query, QueryType, RawQuery, Result, Value};
101
102use std::borrow::Cow;
103
104use keelson_core::ToValue;
105use keelson_core::expr;
106
107// ---------------------------------------------------------------------------
108// Statement starters
109// ---------------------------------------------------------------------------
110
111/// Build a `SELECT` from one mod — usually a tuple of them.
112///
113/// The returned query knows its own dialect, so
114/// [`build()`](keelson_core::Query::build) takes no arguments. bob wraps its query
115/// in a `BaseQuery` to carry the dialect; here the query type carries it itself.
116pub fn select(mods: impl Mod<SelectQuery>) -> SelectQuery {
117    let mut q = SelectQuery::default();
118    mods.apply(&mut q);
119    q
120}
121
122/// Build an `INSERT` from one mod.
123pub fn insert(mods: impl Mod<InsertQuery>) -> InsertQuery {
124    let mut q = InsertQuery::default();
125    mods.apply(&mut q);
126    q
127}
128
129/// Build an `UPDATE` from one mod.
130pub fn update(mods: impl Mod<UpdateQuery>) -> UpdateQuery {
131    let mut q = UpdateQuery::default();
132    mods.apply(&mut q);
133    q
134}
135
136/// Build a `DELETE` from one mod.
137pub fn delete(mods: impl Mod<DeleteQuery>) -> DeleteQuery {
138    let mut q = DeleteQuery::default();
139    mods.apply(&mut q);
140    q
141}
142
143/// Build a `MERGE` from one mod (PostgreSQL 15+).
144///
145/// The grammar requires a target ([`merge::into`]), a source ([`merge::using`]),
146/// an [`merge::on`] condition and at least one `WHEN` clause; a `MERGE` missing
147/// any of them is a [`build()`](keelson_core::Query::build) error naming the
148/// absent piece.
149pub fn merge(mods: impl Mod<MergeQuery>) -> MergeQuery {
150    let mut q = MergeQuery::default();
151    mods.apply(&mut q);
152    q
153}
154
155/// Build a standalone `VALUES` statement from one mod.
156///
157/// The rows come from [`values::row`]/[`values::rows`]; with none the statement
158/// is a [`build()`](keelson_core::Query::build) error, because `VALUES` with no
159/// rows is not a statement.
160pub fn values(mods: impl Mod<ValuesQuery>) -> ValuesQuery {
161    let mut q = ValuesQuery::default();
162    mods.apply(&mut q);
163    q
164}
165
166/// Build a `TABLE name` command from one mod — PostgreSQL's shorthand for
167/// `SELECT * FROM name`.
168///
169/// The table comes from [`table::name`]; with none the statement is a
170/// [`build()`](keelson_core::Query::build) error.
171pub fn table(mods: impl Mod<TableQuery>) -> TableQuery {
172    let mut q = TableQuery::default();
173    mods.apply(&mut q);
174    q
175}
176
177// ---------------------------------------------------------------------------
178// Expression starters
179// ---------------------------------------------------------------------------
180
181/// A whole statement, written by hand, as a runnable query.
182///
183/// [`raw`] is a *fragment* an expression accepts; this is a *statement* nothing
184/// built. It is an ordinary [`Query`], so the execution layer's verbs work on
185/// it — `fetch_all::<T>()` maps hand-written PostgreSQL onto a struct exactly as
186/// it maps a built one — and it nests as a sub-select in a built statement.
187///
188/// Placeholders are `?` and are rewritten to `$1` as it renders; `\?` is a
189/// literal question mark. Values are bound with [`RawQuery::bind`] and never
190/// reach the SQL text.
191///
192/// ```
193/// use keelson_psql::{raw_query, Query as _};
194///
195/// let q = raw_query("SELECT id, name FROM users WHERE age >= ?").bind(21);
196/// let (sql, args) = q.build()?;
197/// assert_eq!(sql, "SELECT id, name FROM users WHERE age >= $1");
198/// assert_eq!(args, vec![keelson_psql::Value::I32(21)]);
199/// # Ok::<_, keelson_psql::Error>(())
200/// ```
201pub fn raw_query(sql: impl Into<Cow<'static, str>>) -> RawQuery<Psql> {
202    RawQuery::new(Psql, sql)
203}
204
205/// [`raw_query`], with the values written where they bind (feature `macros`).
206///
207/// ```
208/// # use keelson_psql::{sql, Query as _};
209/// let min_age = 21;
210/// let q = sql!("SELECT id, name FROM users WHERE age >= {min_age}");
211/// let (text, args) = q.build()?;
212/// assert_eq!(text, "SELECT id, name FROM users WHERE age >= $1");
213/// assert_eq!(args, vec![keelson_psql::Value::I32(21)]);
214/// # Ok::<_, keelson_psql::Error>(())
215/// ```
216///
217/// It expands to exactly that call — `raw_query("…").bind(min_age)` — so
218/// nothing is hidden and the result composes like any other query. What it
219/// buys is two mistakes that stop being expressible:
220///
221/// - **Binds cannot be transposed.** `raw_query(…).bind(a).bind(b)` with `a`
222///   and `b` the wrong way round type-checks and runs; here the value is
223///   written at the hole.
224/// - **A question mark you typed stays a question mark.** The `?` rewriting
225///   does not track quoting, so `WHERE note = 'what\?'` would otherwise hold a
226///   hole, and a statement whose argument count happened to match would be
227///   silently wrong. The macro escapes every `?` it did not generate.
228///
229/// The grammar is `format!`'s, and the analogy is exact except in one place
230/// that matters: **`{x}` binds, it never interpolates.** No hole can put text
231/// into the SQL. Where you do want to splice SQL — an `IN` list, a sub-query
232/// — say so with `{x:sql}`, which takes an expression rather than a value:
233///
234/// ```
235/// # use keelson_psql::{args, sql, Query as _};
236/// let ids = args([1, 2, 3]);
237/// let (text, _) = sql!("SELECT * FROM users WHERE id IN ({ids:sql})").build()?;
238/// assert!(text.contains("IN ("));
239/// # Ok::<_, keelson_psql::Error>(())
240/// ```
241///
242/// `{{` and `}}` are literal braces. Values are still bound, not typed: for
243/// SQL the schema checks, use a generated model or a `.sql` file.
244#[cfg(feature = "macros")]
245#[macro_export]
246macro_rules! sql {
247    ($($tt:tt)*) => {
248        $crate::__sql_with!($crate::raw_query, $($tt)*)
249    };
250}
251
252#[cfg(feature = "macros")]
253#[doc(hidden)]
254pub use keelson_core::__sql_with;
255
256/// Raw SQL, verbatim. `?` is left alone — see [`template`].
257///
258/// The progressive-enhancement entry point: a hand-written fragment goes anywhere a
259/// structured expression does.
260pub fn raw(sql: impl Into<Cow<'static, str>>) -> Expr {
261    expr::raw(sql)
262}
263
264/// Raw SQL whose `?` are rewritten to `$1`, `$2`, … with `args` interleaved. Write
265/// `\?` for a literal question mark.
266pub fn template(sql: impl Into<Cow<'static, str>>, args: impl IntoIterator<Item = RawArg>) -> Expr {
267    expr::template(sql, args)
268}
269
270/// A single-quoted string literal — bob's `S`. `s("A")` renders `'A'`.
271///
272/// Nothing is escaped: this is for SQL the program itself wrote — a keyword, an enum
273/// label. Text from outside belongs in [`arg`], where it is bound.
274pub fn s(literal: impl Into<Cow<'static, str>>) -> Expr {
275    expr::literal(literal)
276}
277
278/// A quoted identifier: `quote("age")` gives `"age"`, `quote(("users", "id"))` gives
279/// `"users"."id"`.
280pub fn quote(parts: impl IntoIdent) -> Expr {
281    expr::quote(parts)
282}
283
284/// One bound argument, rendered `$n`.
285pub fn arg(value: impl ToValue) -> Expr {
286    expr::arg(value)
287}
288
289/// Several bound arguments, comma-separated and *not* parenthesised — for a slot
290/// that brings its own, such as `VALUES (…)`.
291pub fn args<V: ToValue>(values: impl IntoIterator<Item = V>) -> Expr {
292    expr::args(values)
293}
294
295/// Several bound arguments, parenthesised: `($1, $2, $3)`.
296pub fn arg_group<V: ToValue>(values: impl IntoIterator<Item = V>) -> Expr {
297    expr::arg_group(values)
298}
299
300/// `n` unbound placeholders, each binding `NULL`, so a statement can be prepared
301/// now and its values supplied by whatever rebinds it.
302pub fn placeholders(n: usize) -> Expr {
303    expr::placeholders(n)
304}
305
306/// A parenthesised, comma-separated list: `(a, b)`. One element gives plain
307/// parentheses.
308pub fn group(items: impl IntoExprList) -> Expr {
309    expr::group(items)
310}
311
312/// A function call: `f("count", "*")`, `f("row_number", ()).over(())`.
313///
314/// Returns keelson-psql's own [`Function`], which carries `DISTINCT`,
315/// `ORDER BY`, `WITHIN GROUP`, `FILTER`, column definitions and `OVER` — everything
316/// PostgreSQL hangs off a call and core deliberately does not know about.
317pub fn f(name: impl Into<Cow<'static, str>>, args: impl IntoExprList) -> Function {
318    Function::new(name, args)
319}
320
321/// A `CASE` expression: `case_().when(cond, then).else_(other)`.
322///
323/// Named with a trailing underscore because `case` is not available as a plain
324/// identifier in a way that reads well next to `match`; the SQL is unaffected.
325pub fn case_() -> CaseBuilder {
326    expr::case()
327}
328
329/// `CAST(expr AS type_name)`. [`PsqlOps::cast_to`] is the `::` shorthand.
330///
331/// Not wrapped in parentheses of its own: `CAST(…)` is already self-delimiting, so
332/// a wrapping pair could never disambiguate anything.
333pub fn cast(expression: impl IntoExpr, type_name: impl Into<Cow<'static, str>>) -> Expr {
334    expr::cast(expression, type_name)
335}
336
337/// `NOT expr`. The operand is parenthesised if it needs it; the result is not,
338/// because `NOT` binds looser than anything it can contain.
339pub fn not(expression: impl IntoExpr) -> Expr {
340    expr::not(expression)
341}
342
343/// `(a AND b AND c)`.
344pub fn and(items: impl IntoExprList) -> Expr {
345    expr::and(items)
346}
347
348/// `(a OR b OR c)`.
349pub fn or(items: impl IntoExprList) -> Expr {
350    expr::or(items)
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn a_starter_takes_no_mods_at_all() {
359        // `()` is a mod, so the empty query needs no separate constructor.
360        assert_eq!(select(()).build().unwrap().0, "SELECT *");
361    }
362
363    #[test]
364    fn apply_adds_mods_to_a_built_query_and_clone_leaves_the_original_alone() {
365        let base = select((select::columns(quote("id")), select::from(quote("users"))));
366        let mut narrowed = base.clone();
367        narrowed.apply(select::where_(quote("id").eq(arg(1i32))));
368
369        assert_eq!(
370            base.build().unwrap().0,
371            r#"SELECT "id" FROM "users""#,
372            "the clone did not disturb the original"
373        );
374        assert_eq!(
375            narrowed.build().unwrap().0,
376            r#"SELECT "id" FROM "users" WHERE ("id" = $1)"#
377        );
378    }
379
380    #[test]
381    fn the_query_type_is_carried_rather_than_reparsed() {
382        assert_eq!(select(()).query_type(), QueryType::Select);
383        assert_eq!(insert(()).query_type(), QueryType::Insert);
384        assert_eq!(update(()).query_type(), QueryType::Update);
385        assert_eq!(delete(()).query_type(), QueryType::Delete);
386        assert_eq!(merge(()).query_type(), QueryType::Merge);
387        // VALUES and TABLE are SELECT shorthands: rows come back.
388        assert_eq!(values(()).query_type(), QueryType::Select);
389        assert_eq!(table(()).query_type(), QueryType::Select);
390    }
391
392    #[test]
393    fn a_statement_missing_a_clause_it_cannot_render_without_says_so() {
394        // The substrings name the SQL concepts, not the message wording.
395        let err = insert(()).build().unwrap_err();
396        assert!(
397            matches!(&err, Error::Incomplete(what) if what.contains("INSERT")),
398            "got: {err}"
399        );
400        let err = update(update::table(quote("users"))).build().unwrap_err();
401        assert!(
402            matches!(&err, Error::Incomplete(what)
403                if what.contains("assignments") && what.contains("UPDATE")),
404            "got: {err}"
405        );
406        let err = delete(()).build().unwrap_err();
407        assert!(
408            matches!(&err, Error::Incomplete(what) if what.contains("DELETE")),
409            "got: {err}"
410        );
411    }
412}