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//! The broker's in-process mode, which the test harness connects with, counts the rows a
45//! subscription can claim as it opens with [`Dialect::count_claimable`], so the harness waits for
46//! the rows a service wrote before the test.
47//!
48//! A table may open its transactions at an isolation level ([`Isolation`]) or, on SQLite, in a
49//! mode ([`Mode`]): its [`Opening`], set with [`TableSpec::isolation`] and [`TableSpec::mode`].
50//! [`Dialect::begin`] gives the statement that opens a transaction at it, and refuses an opening
51//! the database lacks. A dialect also states the openings it serves as types, one [`Opens`]
52//! implementation per [`level`] type, so a table that names a level its dialect lacks does not
53//! compile.
54//!
55//! A transactional outbox table records what a service publishes until a consumer has processed
56//! it. [`OutboxDialect`] builds its statements: the take of a record by its id, the mark of a
57//! processed record, and the recovery of the unprocessed records of one name, which the table's
58//! [`group`](TableSpec::group) column holds. Every built-in dialect implements it.
59//!
60//! [`Postgres`], [`MySql`] and [`Sqlite`] are built in, behind the `postgres`, `mysql` and
61//! `sqlite` features. [`Postgres`] and [`MySql`] implement [`RowLock`], [`Lease`] and
62//! [`Advisory`], and [`MySql`] serves MariaDB too; [`Sqlite`] implements [`Lease`] and
63//! [`Advisory`]. Their inserts are written by the `const fn`s of [`insert`], so a service that
64//! describes a table by hand renders its insert in a `const`, as the derive does. A database
65//! without a built-in dialect takes a type of the service's own: it implements [`Dialect`], the
66//! trait of each form it builds and [`Opens`] for each level it opens, and writes every statement
67//! in its database's SQL. A service on a built-in dialect that writes
68//! one statement its own way writes it as an event of its inbox: `#[inbox(custom(ack))]` and an
69//! `Ack` implementation of its own, in
70//! [`ruststream-sqlx`](https://powersemmi.github.io/ruststream-sqlx/latest/components/tables/events/).
71//!
72//! # Examples
73//!
74//! A dialect for SQL Server, a database without a built-in one, in the row lock form. The service
75//! mounts it with `SqlxBroker::with_dialect`, as the [`ruststream-sqlx`
76//! site](https://powersemmi.github.io/ruststream-sqlx/latest/components/databases/dialect/)
77//! shows.
78//!
79//! ```
80//! # use std::num::NonZeroUsize;
81//! use ruststream_sqlx_dialect::{
82//! ClaimShape, Dialect, Opening, Param, RowLock, Statement, StatementError, TableSpec,
83//! };
84//! # use ruststream_sqlx_dialect::TableName;
85//!
86//! /// SQL Server: bracket-quoted names, `@p1` placeholders, rows claimed under an update lock that
87//! /// skips the rows another claim holds.
88//! #[derive(Debug)]
89//! pub struct Mssql;
90//!
91//! impl Dialect for Mssql {
92//! fn name(&self) -> &'static str {
93//! "mssql"
94//! }
95//!
96//! fn quote_into(&self, ident: &str, out: &mut String) {
97//! out.push('[');
98//! out.push_str(&ident.replace(']', "]]"));
99//! out.push(']');
100//! }
101//!
102//! fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) {
103//! out.push_str("@p");
104//! out.push_str(&index.to_string());
105//! }
106//!
107//! fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
108//! let mut sql = String::from("DELETE FROM ");
109//! self.quote_into(spec.table(), &mut sql);
110//! sql.push_str(" WHERE ");
111//! self.quote_into(spec.id().name(), &mut sql);
112//! sql.push_str(" = ");
113//! self.placeholder_into(NonZeroUsize::MIN, &mut sql);
114//! Ok(Statement::new(sql, [Param::Id]))
115//! }
116//!
117//! // SQL Server starts a transaction with `BEGIN TRANSACTION`.
118//! fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
119//! match opening {
120//! Opening::Default => Ok(Some("BEGIN TRANSACTION")),
121//! other => Err(StatementError::UnsupportedOpening {
122//! dialect: self.name(),
123//! opening: other.name(),
124//! }),
125//! }
126//! }
127//!
128//! // The statements this service never runs refuse the table.
129//! # fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
130//! # fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
131//! # fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
132//! # fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
133//! # fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
134//! # fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
135//! # fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
136//! }
137//!
138//! // The row lock form, which the service's `email_jobs` table takes.
139//! impl RowLock for Mssql {
140//! fn lock_claim(
141//! &self,
142//! _spec: &TableSpec<'_>,
143//! _shape: ClaimShape,
144//! ) -> Result<Statement, StatementError> {
145//! Ok(Statement::new(
146//! "SELECT TOP (@p1) [job_id], [payload] FROM [email_jobs] \
147//! WITH (UPDLOCK, READPAST, ROWLOCK) ORDER BY [job_id]",
148//! [Param::Limit],
149//! ))
150//! }
151//! }
152//! ```
153
154#![forbid(unsafe_code)]
155
156mod advisory;
157mod column;
158mod dialect;
159mod form;
160#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
161pub mod insert;
162mod lease;
163#[cfg(feature = "mysql")]
164mod mysql;
165mod opening;
166mod outbox;
167#[cfg(feature = "postgres")]
168mod postgres;
169mod role;
170mod row_lock;
171mod spec;
172#[cfg(feature = "sqlite")]
173mod sqlite;
174mod statement;
175mod table_name;
176#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
177mod writer;
178
179pub use advisory::Advisory;
180pub use column::Column;
181pub use dialect::Dialect;
182pub use form::{Form, KeyPart};
183pub use lease::Lease;
184#[cfg(feature = "mysql")]
185pub use mysql::MySql;
186pub use opening::{Isolation, Mode, Opening, Opens, level};
187pub use outbox::OutboxDialect;
188#[cfg(feature = "postgres")]
189pub use postgres::Postgres;
190pub use role::Role;
191pub use row_lock::RowLock;
192pub use spec::TableSpec;
193#[cfg(feature = "sqlite")]
194pub use sqlite::Sqlite;
195pub use statement::{ClaimShape, NameLimit, Param, Statement, StatementError};
196pub use table_name::{ParseTableNameError, TableName};