compact_sql/
lib.rs

1#![cfg_attr(
2	feature = "pretty-errors",
3	feature(proc_macro_diagnostic, proc_macro_span)
4)]
5
6//! This crate provides the [`pg_sql!`] macro for writing SQL queries (for [PostgreSQL]) in
7//! Rust syntax (not Rust code), i.e. you can use convenient indents, comments, Rust's
8//! string escaping etc. without putting the query into a single string and get some
9//! syntax highlighting (functions call, capitalized reserved words, ...).
10//!
11//! [PostgreSQL]: https://www.postgresql.org/
12//!
13//! On build the macro creates an SQL string with minimum spaces and replace identifiers in curly
14//! braces "SELECT {arg1}::bigint" into sequenced argument number ("$1", "$2", etc.). If there are
15//! several entries of the same named argument, the same numbered argument is used.
16//!
17//! There are two modes: the first is just returns a minificated SQL-string, the second generates
18//! additional code to be used with a query arguments struct (fields must match names and types in
19//! named arguments in the SQL query) and expected return row numbers defined as a specific trait
20//! (to be polished and documented).
21//!
22//! You can find examples in the [`pg_sql!`] description.
23//!
24//! ```toml
25//! [dependencies]
26//! compact_sql = "0.0.5"
27//! ```
28//!
29//! # Features:
30//! | Feature | Description |
31//! | ------- | ----------- |
32//! | pretty-errors | (requires nightly Rust) gives better errors pointing to a concrete span in the SQL query text. |
33//!
34//! # Constraints:
35//! There are several artificial constraints:
36//! 1. expanded asterisks (`SELECT * FROM...`) are forbidden;
37//! 1. direct numbered arguments (`$1`, `$2`, etc.) are forbidden;
38//! 1. DBMS objects (schema/table/view/index/etc. names) can not be quoted
39//! (like `SELECT ... FROM "CaseSensitiveTable" ...`).
40//!
41//! Instead you can use leading commas (which are forbidden by the SQL standard) which will not
42//! appear in resulting SQL query.
43//!
44//! # Generate SQL string and struct with SQL params
45//! The macros replaces "named parameters" with numbered parameters for Postgres, and it is
46//! reasonable to create struct where attributes names are the same as names parameters. Moreover it
47//! is easy to constraint such parameters with Rust's types. Struct will have a reference to the
48//! declared type (see example below).
49//!
50//! It is designed to be used with the [`tokio-postgres`][tokio-postgres] crate, the method `params`
51//! of the generated struct returns an iterator for [tokio_postgres::query_raw][tokio-query_raw].
52//!
53//! To associate each numbered parameter with its corresponding field name, the struct contains
54//! a const slice `::PARAM_NAMES`. The names are listed in the order they appear in the SQL query.
55//!
56//! ```rust
57//! use compact_sql::pg_sql;
58//!
59//! pg_sql! {
60//!     impl OidFromPgClass {
61//!         SELECT
62//!             oid,
63//!             relkind,
64//!         FROM
65//!             pg_catalog.pg_class
66//!         WHERE
67//!             relname = {name: String}
68//!             AND
69//!             relnamespace = {nsp: u32}
70//!     }
71//! }
72//!
73//! fn main() {
74//!     let name: String = "pg_attribute".to_string();
75//!     let params = OidFromPgClass {
76//!         nsp:  &11,
77//!         name: &name,
78//!     };
79//!     assert_eq!(OidFromPgClass::PARAM_NAMES, &["name", "nsp"]);
80//!     assert_eq!(
81//!         OidFromPgClass::SQL_TEXT,
82//!         concat!(
83//!             "-- OidFromPgClass\n",
84//!             "SELECT oid,relkind FROM pg_catalog.pg_class WHERE relname=$1 AND relnamespace=$2",
85//!         ),
86//!     );
87//!     assert_eq!(params.params().len(), 2);
88//! }
89//! ```
90//!
91//! # Generate SQL, struct with SQL params and traits implementation
92//! This library can generate trait implementation bodies for those who want to build a library
93//! (or internal functions) that work with generated structs. Not all user structs are designed
94//! to be public, which is why the generated trait implementations are not referenced in a public
95//! crate. Instead, trait names are used from the local scope. However, you can still use traits
96//! from the public [`compact_sql_traits`][compact_sql_traits] crate.
97//!
98//! You can specify the expected number of rows, and different traits will be used accordingly.
99//! Additionally, you must declare the return row type. You can use the
100//! [PgResultRow][derive@PgResultRow] derive macro to generate the necessary implementation.
101//!
102//! ```rust
103//! use compact_sql::{pg_sql, PgResultRow};
104//! use compact_sql_traits::*;
105//! use futures_util::{pin_mut, StreamExt, TryStreamExt};
106//! use tokio_postgres::Client;
107//!
108//! type PgResult<T> = Result<T, tokio_postgres::Error>;
109//!
110//! #[derive(Debug, PgResultRow)]
111//! struct PgClassItem {
112//!     relname:      String,
113//!     relnamespace: u32,
114//! }
115//!
116//! #[derive(Debug)]
117//! enum QueriesId {
118//!     GetPgClassItem,
119//! }
120//!
121//! pg_sql! {
122//!         // You can add container attributes like derive to the generated struct:
123//!         #[derive(Debug)]
124//!         impl QueriesId::GetPgClassItem for ?PgClassItem {
125//!             SELECT
126//!                 relname,
127//!                 relnamespace,
128//!             FROM
129//!                 pg_catalog.pg_class
130//!             WHERE
131//!                 oid = {class_oid}
132//!         }
133//! }
134//!
135//! async fn query_opt<Q: QueryMaybeOne>(db: &Client, params: Q) -> PgResult<Option<Q::ResultRow>>
136//! where <Q as QueryMaybeOne>::SqlIdType: std::fmt::Debug
137//! {
138//!     dbg!(Q::SQL_ID);
139//!     let query_result = db
140//!         .query_raw(Q::SQL_TEXT, params.params())
141//!         .await?;
142//!     let future = query_result.map_ok(Q::ResultRow::from_row);
143//!     pin_mut!(future);
144//!
145//!     Ok(match future.next().await {
146//!         None => None,
147//!         Some(v) => Some(v??),
148//!     })
149//! }
150//!
151//! async fn func(db: &Client) -> PgResult<()> {
152//!     let name: String = "pg_attribute".to_string();
153//!     let params = GetPgClassItem {
154//!         class_oid: &11,
155//!     };
156//!     let res: Option<PgClassItem> = query_opt(db, params).await?;
157//!     dbg!(res);
158//!
159//!     Ok(())
160//! }
161//! ```
162//!
163//! You can see more examples in the `test_trait_impl.rs`.
164//!
165//! # Test query in real DBMS
166//! Your queries can be tested (via "prepare" feature of the PG's protocol,
167//! i.e. does not run any query at all;
168//! totally safe) in a real DBMS if you set environment variables for `cargo`:
169//! * `TEST_PG_HOST`
170//! * `TEST_PG_PORT` (optional; default: 5432)
171//! * `TEST_PG_USER` (optional; default: "postgres")
172//! * `TEST_PG_PASS` (optional; default: "")
173//! * `TEST_PG_DATB`
174//!
175//! It allows to detect wrong SQL for your DB schema. E.g.:
176//!
177//! ```shell
178//! TEST_PG_HOST=127.0.0.1 TEST_PG_DATB=test_db cargo check
179//! ```
180//! [compact_sql_traits]: https://crates.io/crates/compact_sql_traits
181//! [tokio-postgres]: https://crates.io/crates/tokio-postgres
182//! [tokio-query_raw]: https://docs.rs/tokio-postgres/latest/tokio_postgres/struct.Client.html#method.query_raw
183
184
185#[macro_use]
186extern crate quote;
187
188use proc_macro::TokenStream;
189
190mod from_row;
191mod sql;
192
193/// The whole point
194///
195/// # Examples:
196///
197/// ## Generate just SQL string
198/// ```
199/// # use compact_sql::pg_sql;
200/// #
201/// # fn main() {
202/// let sql = pg_sql! {
203///     SELECT
204///         relname,
205///         relnamespace,
206///         format("%I", oid),
207///     FROM
208///         pg_catalog.pg_class
209///     WHERE
210///         oid = {var_oid}::oid
211/// };
212/// assert_eq!(
213///     sql,
214///     "SELECT relname,relnamespace,format('%I',oid)FROM pg_catalog.pg_class WHERE oid=$1::oid"
215/// );
216/// # }
217/// ```
218#[proc_macro]
219pub fn pg_sql(input: TokenStream) -> TokenStream {
220	let mut ret = sql::SqlHandler::new();
221	match ret.parse(input) {
222		Ok(()) => ret.finish(),
223		Err(()) => TokenStream::default(),
224	}
225}
226
227#[proc_macro_derive(PgResultRow)]
228pub fn row_from_sql(input: TokenStream) -> TokenStream {
229	let ast: syn::DeriveInput = syn::parse(input).unwrap();
230
231	match &ast.data {
232		syn::Data::Struct(ref struct_data) => {
233			let from_sql = from_row::PgResultRow::new(&ast, struct_data);
234			from_sql.implement_derive().into()
235		},
236		_ => panic!("#[derive(PgResultRow)] works only on structs"),
237	}
238}