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}