Skip to main content

ruststream_sqlx_dialect/
opening.rs

1//! What a table's transactions open at: an isolation level, a SQLite mode, or the database's
2//! default; and the levels a dialect opens, as types.
3
4use crate::dialect::Dialect;
5use crate::statement::StatementError;
6
7/// The isolation level a transaction opens at, on Postgres, MySQL and MariaDB.
8///
9/// A table names one ([`TableSpec::isolation`](crate::TableSpec::isolation)), and the dialect's
10/// [`begin`](Dialect::begin) opens the table's transactions at it. A database that runs a level
11/// as a stronger one does not keep it, so its dialect refuses it: Postgres refuses
12/// [`ReadUncommitted`](Self::ReadUncommitted).
13///
14/// # Examples
15///
16/// ```
17/// # use std::num::NonZeroUsize;
18/// # use ruststream_sqlx_dialect::TableName;
19/// use ruststream_sqlx_dialect::{
20///     Dialect, Isolation, Opening, Opens, Statement, StatementError, TableSpec, level,
21/// };
22///
23/// /// SQL Server, a database without a built-in dialect.
24/// #[derive(Debug)]
25/// pub struct Mssql;
26///
27/// impl Dialect for Mssql {
28///     fn name(&self) -> &'static str {
29///         "mssql"
30///     }
31///
32///     fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
33///         match opening {
34///             Opening::Default | Opening::Isolation(Isolation::ReadCommitted) => Ok(Some(
35///                 "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN TRANSACTION",
36///             )),
37///             Opening::Isolation(Isolation::RepeatableRead) => Ok(Some(
38///                 "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN TRANSACTION",
39///             )),
40///             // A refusal names the opening the table declares.
41///             other => Err(StatementError::UnsupportedOpening {
42///                 dialect: self.name(),
43///                 opening: other.name(),
44///             }),
45///         }
46///     }
47/// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
48/// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
49/// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
50/// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
51/// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
52/// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
53/// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
54/// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
55/// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
56/// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
57/// }
58///
59/// // A table at one of these levels mounts on `Mssql`.
60/// impl Opens<level::ReadCommitted> for Mssql {}
61/// impl Opens<level::RepeatableRead> for Mssql {}
62/// ```
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64#[non_exhaustive]
65pub enum Isolation {
66    /// A read may see what other transactions wrote and have not committed.
67    ReadUncommitted,
68    /// Each statement sees what was committed before it began.
69    ReadCommitted,
70    /// Every statement sees what was committed before the transaction's first one.
71    RepeatableRead,
72    /// Transactions take effect as if they ran one after another.
73    Serializable,
74}
75
76impl Isolation {
77    /// The level as SQL names it: `READ COMMITTED`.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// # #[cfg(feature = "postgres")]
83    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
84    /// use ruststream_sqlx_dialect::{Dialect, Isolation, Opening, Postgres};
85    ///
86    /// // A dialect's test: each level it serves opens a transaction at that level.
87    /// for level in [Isolation::ReadCommitted, Isolation::RepeatableRead, Isolation::Serializable] {
88    ///     let begin = Postgres.begin(Opening::Isolation(level))?;
89    ///     assert_eq!(begin, Some(format!("BEGIN ISOLATION LEVEL {}", level.sql())).as_deref());
90    /// }
91    /// # Ok(())
92    /// # }
93    /// # #[cfg(not(feature = "postgres"))]
94    /// # fn main() {}
95    /// ```
96    #[must_use]
97    pub const fn sql(self) -> &'static str {
98        match self {
99            Self::ReadUncommitted => "READ UNCOMMITTED",
100            Self::ReadCommitted => "READ COMMITTED",
101            Self::RepeatableRead => "REPEATABLE READ",
102            Self::Serializable => "SERIALIZABLE",
103        }
104    }
105
106    /// The level as a table names it: `read_committed`.
107    // The derives and the crate read it to parse and check a table; a service never names it.
108    #[doc(hidden)]
109    #[must_use]
110    pub const fn attribute(self) -> &'static str {
111        match self {
112            Self::ReadUncommitted => "read_uncommitted",
113            Self::ReadCommitted => "read_committed",
114            Self::RepeatableRead => "repeatable_read",
115            Self::Serializable => "serializable",
116        }
117    }
118}
119
120/// How a SQLite transaction takes the database's write lock.
121///
122/// SQLite runs every transaction serializable, with one writer at a time, so a table on it names
123/// the moment its transactions take the write lock ([`TableSpec::mode`](crate::TableSpec::mode))
124/// instead of a level.
125///
126/// # Examples
127///
128/// A dialect for a database that takes SQLite's modes opens its transactions in them:
129///
130/// ```
131/// # use std::num::NonZeroUsize;
132/// # use ruststream_sqlx_dialect::TableName;
133/// use ruststream_sqlx_dialect::{
134///     Dialect, Mode, Opening, Opens, Statement, StatementError, TableSpec, level,
135/// };
136///
137/// /// libSQL, a fork of SQLite served over the network, a database without a built-in dialect.
138/// #[derive(Debug)]
139/// pub struct Libsql;
140///
141/// impl Dialect for Libsql {
142///     fn name(&self) -> &'static str {
143///         "libsql"
144///     }
145///
146///     fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
147///         match opening {
148///             Opening::Default => Ok(None),
149///             Opening::Mode(Mode::Deferred) => Ok(Some("BEGIN DEFERRED")),
150///             Opening::Mode(Mode::Immediate) => Ok(Some("BEGIN IMMEDIATE")),
151///             Opening::Mode(Mode::Exclusive) => Ok(Some("BEGIN EXCLUSIVE")),
152///             // libSQL runs every transaction serializable, as SQLite does: it names no level.
153///             other => Err(StatementError::UnsupportedOpening {
154///                 dialect: self.name(),
155///                 opening: other.name(),
156///             }),
157///         }
158///     }
159/// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('"'); out.push_str(&ident.replace('"', "\"\"")); out.push('"'); }
160/// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push('?'); out.push_str(&index.to_string()); }
161/// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
162/// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
163/// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
164/// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
165/// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
166/// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
167/// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
168/// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
169/// }
170///
171/// // A table with `mode = deferred`, `immediate` or `exclusive` mounts on `Libsql`.
172/// impl Opens<level::Deferred> for Libsql {}
173/// impl Opens<level::Immediate> for Libsql {}
174/// impl Opens<level::Exclusive> for Libsql {}
175/// ```
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
177#[non_exhaustive]
178pub enum Mode {
179    /// The transaction takes no lock until it first reads or writes, as a plain `BEGIN` does.
180    Deferred,
181    /// The transaction takes the write lock when it begins, so no other writer comes between its
182    /// reads and its writes.
183    Immediate,
184    /// The transaction takes the write lock when it begins and, outside WAL mode, keeps every
185    /// other connection from reading until it ends.
186    Exclusive,
187}
188
189impl Mode {
190    /// The mode as SQL names it: `IMMEDIATE`.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// # #[cfg(feature = "sqlite")]
196    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
197    /// use ruststream_sqlx_dialect::{Dialect, Mode, Opening, Sqlite};
198    ///
199    /// // A dialect's test: each mode opens a transaction in that mode.
200    /// for mode in [Mode::Deferred, Mode::Immediate, Mode::Exclusive] {
201    ///     let begin = Sqlite.begin(Opening::Mode(mode))?;
202    ///     assert_eq!(begin, Some(format!("BEGIN {}", mode.sql())).as_deref());
203    /// }
204    /// # Ok(())
205    /// # }
206    /// # #[cfg(not(feature = "sqlite"))]
207    /// # fn main() {}
208    /// ```
209    #[must_use]
210    pub const fn sql(self) -> &'static str {
211        match self {
212            Self::Deferred => "DEFERRED",
213            Self::Immediate => "IMMEDIATE",
214            Self::Exclusive => "EXCLUSIVE",
215        }
216    }
217
218    /// The mode as a table names it: `immediate`.
219    // The derives and the crate read it to parse and check a table; a service never names it.
220    #[doc(hidden)]
221    #[must_use]
222    pub const fn attribute(self) -> &'static str {
223        match self {
224            Self::Deferred => "deferred",
225            Self::Immediate => "immediate",
226            Self::Exclusive => "exclusive",
227        }
228    }
229}
230
231/// What a table's transactions open at: the dialect's default, an isolation level, or a SQLite
232/// mode.
233///
234/// A table description carries one ([`TableSpec::opening`](crate::TableSpec::opening)), and the
235/// dialect's [`begin`](Dialect::begin) gives the statement that opens a transaction at it, or
236/// refuses an opening its database lacks.
237///
238/// # Examples
239///
240/// ```
241/// # use std::num::NonZeroUsize;
242/// # use ruststream_sqlx_dialect::TableName;
243/// use ruststream_sqlx_dialect::{
244///     Dialect, Isolation, Opening, Opens, Statement, StatementError, TableSpec, level,
245/// };
246///
247/// /// SQL Server, a database without a built-in dialect.
248/// #[derive(Debug)]
249/// pub struct Mssql;
250///
251/// impl Dialect for Mssql {
252///     fn name(&self) -> &'static str {
253///         "mssql"
254///     }
255///
256///     // SQL Server keeps a level set with `SET TRANSACTION ISOLATION LEVEL` for the rest of the
257///     // session, so a table that names no level opens at READ COMMITTED by name rather than at
258///     // the level the connection last ran.
259///     fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
260///         match opening {
261///             Opening::Default | Opening::Isolation(Isolation::ReadCommitted) => Ok(Some(
262///                 "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN TRANSACTION",
263///             )),
264///             Opening::Isolation(Isolation::RepeatableRead) => Ok(Some(
265///                 "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN TRANSACTION",
266///             )),
267///             Opening::Isolation(Isolation::Serializable) => Ok(Some(
268///                 "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION",
269///             )),
270///             // A SQLite mode, and the levels the service's claims do not run at.
271///             other => Err(StatementError::UnsupportedOpening {
272///                 dialect: self.name(),
273///                 opening: other.name(),
274///             }),
275///         }
276///     }
277/// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
278/// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
279/// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
280/// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
281/// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
282/// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
283/// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
284/// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
285/// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
286/// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
287/// }
288///
289/// impl Opens<level::ReadCommitted> for Mssql {}
290/// impl Opens<level::RepeatableRead> for Mssql {}
291/// impl Opens<level::Serializable> for Mssql {}
292/// ```
293#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
294#[non_exhaustive]
295pub enum Opening {
296    /// What the dialect opens a table's transactions at when the table names neither a level nor
297    /// a mode: the database's own level on Postgres and SQLite, READ COMMITTED on MySQL and
298    /// MariaDB.
299    #[default]
300    Default,
301    /// An isolation level.
302    Isolation(Isolation),
303    /// A SQLite mode.
304    Mode(Mode),
305}
306
307impl Opening {
308    /// The opening as a message names it: `the database's default`, ``isolation `serializable` ``
309    /// or ``mode `immediate` ``.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// # use std::num::NonZeroUsize;
315    /// # use ruststream_sqlx_dialect::TableName;
316    /// use ruststream_sqlx_dialect::{
317    ///     Dialect, Isolation, Opening, Opens, Statement, StatementError, TableSpec, level,
318    /// };
319    ///
320    /// /// SQL Server, a database without a built-in dialect.
321    /// #[derive(Debug)]
322    /// pub struct Mssql;
323    ///
324    /// impl Dialect for Mssql {
325    ///     fn name(&self) -> &'static str {
326    ///         "mssql"
327    ///     }
328    ///
329    ///     fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
330    ///         match opening {
331    ///             Opening::Default | Opening::Isolation(Isolation::ReadCommitted) => Ok(Some(
332    ///                 "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN TRANSACTION",
333    ///             )),
334    ///             Opening::Isolation(Isolation::RepeatableRead) => Ok(Some(
335    ///                 "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN TRANSACTION",
336    ///             )),
337    ///             // A refusal names the opening the table declares.
338    ///             other => Err(StatementError::UnsupportedOpening {
339    ///                 dialect: self.name(),
340    ///                 opening: other.name(),
341    ///             }),
342    ///         }
343    ///     }
344    /// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
345    /// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
346    /// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
347    /// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
348    /// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
349    /// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
350    /// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
351    /// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
352    /// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
353    /// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
354    /// }
355    ///
356    /// // A table at one of these levels mounts on `Mssql`.
357    /// impl Opens<level::ReadCommitted> for Mssql {}
358    /// impl Opens<level::RepeatableRead> for Mssql {}
359    /// ```
360    #[must_use]
361    pub const fn name(self) -> &'static str {
362        match self {
363            Self::Default => "the database's default",
364            Self::Isolation(Isolation::ReadUncommitted) => "isolation `read_uncommitted`",
365            Self::Isolation(Isolation::ReadCommitted) => "isolation `read_committed`",
366            Self::Isolation(Isolation::RepeatableRead) => "isolation `repeatable_read`",
367            Self::Isolation(Isolation::Serializable) => "isolation `serializable`",
368            Self::Mode(Mode::Deferred) => "mode `deferred`",
369            Self::Mode(Mode::Immediate) => "mode `immediate`",
370            Self::Mode(Mode::Exclusive) => "mode `exclusive`",
371        }
372    }
373
374    /// The refusal of the dialect named `dialect`, which opens no transaction at `self`.
375    pub(crate) const fn refused(self, dialect: &'static str) -> StatementError {
376        StatementError::UnsupportedOpening {
377            dialect,
378            opening: self.name(),
379        }
380    }
381}
382
383/// The isolation levels and SQLite modes as types: a table names one, and its dialect opens it
384/// where it implements [`Opens`] for it.
385///
386/// A table that names a level carries it twice: as one of these types, which the bound on its
387/// dialect checks when the service compiles, and as the [`Opening`] in its description, which
388/// [`Dialect::begin`] turns into a statement.
389///
390/// # Examples
391///
392/// A dialect implements `Opens` for each level its `begin` opens:
393///
394/// ```
395/// # use std::num::NonZeroUsize;
396/// # use ruststream_sqlx_dialect::TableName;
397/// use ruststream_sqlx_dialect::{
398///     Dialect, Isolation, Opening, Opens, Statement, StatementError, TableSpec, level,
399/// };
400///
401/// /// SQL Server, a database without a built-in dialect.
402/// #[derive(Debug)]
403/// pub struct Mssql;
404///
405/// impl Dialect for Mssql {
406///     fn name(&self) -> &'static str {
407///         "mssql"
408///     }
409///
410///     fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
411///         match opening {
412///             Opening::Default | Opening::Isolation(Isolation::ReadCommitted) => Ok(Some(
413///                 "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN TRANSACTION",
414///             )),
415///             Opening::Isolation(Isolation::RepeatableRead) => Ok(Some(
416///                 "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN TRANSACTION",
417///             )),
418///             other => Err(StatementError::UnsupportedOpening {
419///                 dialect: self.name(),
420///                 opening: other.name(),
421///             }),
422///         }
423///     }
424/// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
425/// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
426/// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
427/// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
428/// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
429/// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
430/// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
431/// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
432/// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
433/// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
434/// }
435///
436/// // A table at `read_committed` or `repeatable_read` mounts on `Mssql`.
437/// impl Opens<level::ReadCommitted> for Mssql {}
438/// impl Opens<level::RepeatableRead> for Mssql {}
439/// ```
440pub mod level;
441
442/// A dialect that opens transactions at `Level`: an isolation level or a SQLite mode as one of
443/// the [`level`] types, or `()`, which a table that names neither carries.
444///
445/// A subscription requires `Opens<Level>` of its table's dialect, so a table that names a level
446/// its dialect lacks does not compile. Every dialect opens `()`. A dialect implements
447/// `Opens<Level>` for each level whose [`Opening`] its [`begin`](Dialect::begin) opens:
448/// [`Postgres`](crate::Postgres) READ COMMITTED, REPEATABLE READ and SERIALIZABLE;
449/// [`MySql`](crate::MySql) those and READ UNCOMMITTED; [`Sqlite`](crate::Sqlite) its three
450/// modes.
451///
452/// # Examples
453///
454/// A dialect of the service's own opens the levels its `begin` opens:
455///
456/// ```
457/// # use std::num::NonZeroUsize;
458/// # use ruststream_sqlx_dialect::TableName;
459/// use ruststream_sqlx_dialect::{
460///     Dialect, Isolation, Opening, Opens, Statement, StatementError, TableSpec, level,
461/// };
462///
463/// /// SQL Server, a database without a built-in dialect.
464/// #[derive(Debug)]
465/// pub struct Mssql;
466///
467/// impl Dialect for Mssql {
468///     fn name(&self) -> &'static str {
469///         "mssql"
470///     }
471///
472///     fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
473///         match opening {
474///             Opening::Default | Opening::Isolation(Isolation::ReadCommitted) => Ok(Some(
475///                 "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN TRANSACTION",
476///             )),
477///             Opening::Isolation(Isolation::RepeatableRead) => Ok(Some(
478///                 "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN TRANSACTION",
479///             )),
480///             other => Err(StatementError::UnsupportedOpening {
481///                 dialect: self.name(),
482///                 opening: other.name(),
483///             }),
484///         }
485///     }
486/// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
487/// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
488/// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
489/// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
490/// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
491/// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
492/// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
493/// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
494/// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
495/// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
496/// }
497///
498/// // A table at one of these levels mounts on `Mssql`; one at any other level does not compile.
499/// impl Opens<level::ReadCommitted> for Mssql {}
500/// impl Opens<level::RepeatableRead> for Mssql {}
501/// ```
502///
503/// SQLite opens no transaction at an isolation level, so the same subscription on SQLite does
504/// not compile:
505///
506/// ```compile_fail
507/// # #[cfg(not(feature = "sqlite"))]
508/// # compile_error!("the example needs the sqlite feature");
509/// # #[cfg(feature = "sqlite")]
510/// # mod demo {
511/// use ruststream_sqlx_dialect::{Isolation, Opening, Opens, Sqlite, StatementError, level};
512///
513/// fn serializable<D: Opens<level::Serializable>>(
514///     dialect: &D,
515/// ) -> Result<Option<&'static str>, StatementError> {
516///     dialect.begin(Opening::Isolation(Isolation::Serializable))
517/// }
518///
519/// fn subscribe() -> Result<Option<&'static str>, StatementError> {
520///     serializable(&Sqlite)
521/// }
522/// # }
523/// # fn main() {}
524/// ```
525#[diagnostic::on_unimplemented(
526    message = "the `{Self}` dialect opens no transaction at `{Level}`",
527    label = "the table's `isolation` or `mode` names it",
528    note = "Postgres opens `read_committed`, `repeatable_read` and `serializable`; MySQL and \
529            MariaDB those and `read_uncommitted`; SQLite takes the modes `deferred`, \
530            `immediate` and `exclusive`",
531    note = "a dialect of the service's own opens a level by implementing `Opens<Level>` and \
532            `Dialect::begin`"
533)]
534pub trait Opens<Level>: Dialect {}
535
536impl<D: Dialect> Opens<()> for D {}
537
538#[cfg(test)]
539mod tests {
540    use super::{Isolation, Mode, Opening};
541    use crate::statement::StatementError;
542
543    #[test]
544    fn a_level_names_itself_in_sql_and_as_a_table_does() {
545        let levels = [
546            Isolation::ReadUncommitted,
547            Isolation::ReadCommitted,
548            Isolation::RepeatableRead,
549            Isolation::Serializable,
550        ];
551        assert_eq!(
552            levels.map(Isolation::sql),
553            [
554                "READ UNCOMMITTED",
555                "READ COMMITTED",
556                "REPEATABLE READ",
557                "SERIALIZABLE"
558            ]
559        );
560        assert_eq!(
561            levels.map(Isolation::attribute),
562            [
563                "read_uncommitted",
564                "read_committed",
565                "repeatable_read",
566                "serializable"
567            ]
568        );
569    }
570
571    #[test]
572    fn a_mode_names_itself_in_sql_and_as_a_table_does() {
573        let modes = [Mode::Deferred, Mode::Immediate, Mode::Exclusive];
574        assert_eq!(modes.map(Mode::sql), ["DEFERRED", "IMMEDIATE", "EXCLUSIVE"]);
575        assert_eq!(
576            modes.map(Mode::attribute),
577            ["deferred", "immediate", "exclusive"]
578        );
579    }
580
581    #[test]
582    fn an_opening_names_itself_for_messages() {
583        let openings = [
584            Opening::default(),
585            Opening::Isolation(Isolation::ReadUncommitted),
586            Opening::Isolation(Isolation::ReadCommitted),
587            Opening::Isolation(Isolation::RepeatableRead),
588            Opening::Isolation(Isolation::Serializable),
589            Opening::Mode(Mode::Deferred),
590            Opening::Mode(Mode::Immediate),
591            Opening::Mode(Mode::Exclusive),
592        ];
593        assert_eq!(
594            openings.map(Opening::name),
595            [
596                "the database's default",
597                "isolation `read_uncommitted`",
598                "isolation `read_committed`",
599                "isolation `repeatable_read`",
600                "isolation `serializable`",
601                "mode `deferred`",
602                "mode `immediate`",
603                "mode `exclusive`",
604            ]
605        );
606    }
607
608    #[test]
609    fn a_refusal_names_the_dialect_and_the_opening() {
610        assert_eq!(
611            Opening::Mode(Mode::Exclusive).refused("postgres"),
612            StatementError::UnsupportedOpening {
613                dialect: "postgres",
614                opening: "mode `exclusive`",
615            }
616        );
617    }
618}