Skip to main content

ruststream_sqlx_dialect/
lib.rs

1//! SQL text generation for [`ruststream-sqlx`](https://docs.rs/ruststream-sqlx), the SQL database
2//! crate of the [RustStream](https://github.com/powersemmi/ruststream) messaging framework.
3//!
4//! The procedural macros of `ruststream-sqlx` and its runtime build their statements here, so both
5//! produce the same SQL for a table. The crate generates plain text, free of any database driver,
6//! so a procedural macro can run it at compile time.
7//!
8//! A [`TableSpec`] describes a queue table: its name, the [`Column`] that identifies a row, one
9//! slot per [`Role`], the columns of the message's data, and the [`Form`] in which rows are
10//! claimed. The id is part of the constructor, every role has one slot, and the form carries its
11//! own columns, so a table without an id, with a role played twice or with two forms cannot be
12//! described. A dialect turns the description into the [`Statement`] each queue event runs; a
13//! statement carries the [`Param`]s its placeholders bind, in order.
14//!
15//! A dialect is a set of traits. [`Dialect`] quotes names, numbers placeholders and builds the
16//! statements every form runs to settle a row. Each form of claiming is a trait over it:
17//! [`RowLock`] builds the claim that locks rows for the transaction a handler settles in,
18//! [`Lease`] the claim that writes a lease into each row and commits, with the lease's extension,
19//! and [`Advisory`] the claim of candidates with their lock keys, the lock and the unlock of a key,
20//! and the take of a row whose key the session holds. A dialect implements the traits of the forms
21//! its database serves, and a table in a form its dialect lacks does not compile.
22//!
23//! In the lease form a claim writes the lease's expiry ([`Param::Lease`]) into each row it takes
24//! and commits, and skips every row whose lease has not ended by [`Param::LeaseNow`]. The expiry
25//! it wrote is the delivery's ownership token ([`Param::Held`]): a settlement, or an extension
26//! that moves the expiry forward ([`Lease::extend`]), passes only while the row still holds it. A
27//! dialect whose claim only selects the rows says so ([`Lease::claim_writes_lease`]), and each
28//! claimed row is then stamped with its lease ([`Lease::stamp`]).
29//!
30//! In the advisory lock form a session lock on each row's key holds the row. The key's parts
31//! ([`KeyPart`]) name literal text and columns, and the database renders each row's key from them
32//! as text. A claim selects candidates with their keys ([`Advisory::advisory_claim`]), takes the
33//! lock on a key ([`Advisory::lock`], bound as [`Param::Key`]), then the row while it is still
34//! claimable ([`Advisory::take`]); the unlock ([`Advisory::unlock`]) follows the settlement. The
35//! settlements name the row alone, and a retry needs no statement: the take counted the attempt,
36//! and the unlock frees the row.
37//!
38//! A table with FIFO groups ([`TableSpec::fifo_group`]) keeps one row of a group in work. Its
39//! claim takes the group's head, the first unfinished row of the group in claim order, or
40//! nothing. The claim's transaction first takes the group with the statement
41//! [`Dialect::fifo_guard`] gives, so a row that enters the group ahead of the head in work waits
42//! for it; a lease claim also takes nothing while a row of the group holds a lease.
43//!
44//! A table may open its transactions at an isolation level ([`Isolation`]) or, on SQLite, in a
45//! mode ([`Mode`]): its [`Opening`], set with [`TableSpec::isolation`] and [`TableSpec::mode`].
46//! [`Dialect::begin`] gives the statement that opens a transaction at it, and refuses an opening
47//! the database lacks. A dialect also states the openings it serves as types, one [`Opens`]
48//! implementation per [`level`] type, so a table that names a level its dialect lacks does not
49//! compile.
50//!
51//! A transactional outbox table records what a service publishes until a consumer has processed
52//! it. [`OutboxDialect`] builds its statements: the take of a record by its id, the mark of a
53//! processed record, and the recovery of the unprocessed records of one name, which the table's
54//! [`group`](TableSpec::group) column holds. Every built-in dialect implements it.
55//!
56//! [`Postgres`], [`MySql`] and [`Sqlite`] are built in, behind the `postgres`, `mysql` and
57//! `sqlite` features. [`Postgres`] and [`MySql`] implement [`RowLock`], [`Lease`] and
58//! [`Advisory`], and [`MySql`] serves MariaDB too; [`Sqlite`] implements [`Lease`] and
59//! [`Advisory`]. Their inserts are written by the `const fn`s of [`insert`], so a service that
60//! describes a table by hand renders its insert in a `const`, as the derive does. A database
61//! without a built-in dialect takes a type of the service's own: it implements [`Dialect`], the
62//! trait of each form it builds and [`Opens`] for each level it opens, and writes every statement
63//! in its database's SQL. A service on a built-in dialect that writes
64//! one statement its own way writes it as an event of its inbox: `#[inbox(custom(ack))]` and an
65//! `Ack` implementation of its own, in
66//! [`ruststream-sqlx`](https://docs.rs/ruststream-sqlx/latest/ruststream_sqlx/index.html#an-event-of-the-services-own).
67//!
68//! # Examples
69//!
70//! A dialect for SQL Server, a database without a built-in one, in the row lock form. The service
71//! mounts it with `SqlxBroker::with_dialect`, as the [`ruststream-sqlx`
72//! overview](https://docs.rs/ruststream-sqlx/latest/ruststream_sqlx/#a-dialect-of-the-services-own)
73//! shows.
74//!
75//! ```
76//! # use std::num::NonZeroUsize;
77//! use ruststream_sqlx_dialect::{
78//!     ClaimShape, Dialect, Opening, Param, RowLock, Statement, StatementError, TableSpec,
79//! };
80//! # use ruststream_sqlx_dialect::TableName;
81//!
82//! /// SQL Server: bracket-quoted names, `@p1` placeholders, rows claimed under an update lock that
83//! /// skips the rows another claim holds.
84//! #[derive(Debug)]
85//! pub struct Mssql;
86//!
87//! impl Dialect for Mssql {
88//!     fn name(&self) -> &'static str {
89//!         "mssql"
90//!     }
91//!
92//!     fn quote_into(&self, ident: &str, out: &mut String) {
93//!         out.push('[');
94//!         out.push_str(&ident.replace(']', "]]"));
95//!         out.push(']');
96//!     }
97//!
98//!     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) {
99//!         out.push_str("@p");
100//!         out.push_str(&index.to_string());
101//!     }
102//!
103//!     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
104//!         let mut sql = String::from("DELETE FROM ");
105//!         self.quote_into(spec.table(), &mut sql);
106//!         sql.push_str(" WHERE ");
107//!         self.quote_into(spec.id().name(), &mut sql);
108//!         sql.push_str(" = ");
109//!         self.placeholder_into(NonZeroUsize::MIN, &mut sql);
110//!         Ok(Statement::new(sql, [Param::Id]))
111//!     }
112//!
113//!     // SQL Server starts a transaction with `BEGIN TRANSACTION`.
114//!     fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
115//!         match opening {
116//!             Opening::Default => Ok(Some("BEGIN TRANSACTION")),
117//!             other => Err(StatementError::UnsupportedOpening {
118//!                 dialect: self.name(),
119//!                 opening: other.name(),
120//!             }),
121//!         }
122//!     }
123//!
124//!     // The statements this service never runs refuse the table.
125//! #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
126//! #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
127//! #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
128//! #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
129//! #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
130//! #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
131//! #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
132//! }
133//!
134//! // The row lock form, which the service's `email_jobs` table takes.
135//! impl RowLock for Mssql {
136//!     fn lock_claim(
137//!         &self,
138//!         _spec: &TableSpec<'_>,
139//!         _shape: ClaimShape,
140//!     ) -> Result<Statement, StatementError> {
141//!         Ok(Statement::new(
142//!             "SELECT TOP (@p1) [job_id], [payload] FROM [email_jobs] \
143//!              WITH (UPDLOCK, READPAST, ROWLOCK) ORDER BY [job_id]",
144//!             [Param::Limit],
145//!         ))
146//!     }
147//! }
148//! ```
149
150#![forbid(unsafe_code)]
151
152mod advisory;
153mod column;
154mod dialect;
155mod form;
156#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
157pub mod insert;
158mod lease;
159#[cfg(feature = "mysql")]
160mod mysql;
161mod opening;
162mod outbox;
163#[cfg(feature = "postgres")]
164mod postgres;
165mod role;
166mod row_lock;
167mod spec;
168#[cfg(feature = "sqlite")]
169mod sqlite;
170mod statement;
171mod table_name;
172#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
173mod writer;
174
175pub use advisory::Advisory;
176pub use column::Column;
177pub use dialect::Dialect;
178pub use form::{Form, KeyPart};
179pub use lease::Lease;
180#[cfg(feature = "mysql")]
181pub use mysql::MySql;
182pub use opening::{Isolation, Mode, Opening, Opens, level};
183pub use outbox::OutboxDialect;
184#[cfg(feature = "postgres")]
185pub use postgres::Postgres;
186pub use role::Role;
187pub use row_lock::RowLock;
188pub use spec::TableSpec;
189#[cfg(feature = "sqlite")]
190pub use sqlite::Sqlite;
191pub use statement::{ClaimShape, NameLimit, Param, Statement, StatementError};
192pub use table_name::{ParseTableNameError, TableName};