Skip to main content

ruststream_sqlx_dialect/
statement.rs

1//! What a dialect produces: the text of a statement and the values its placeholders bind.
2
3mod error;
4
5pub use error::{NameLimit, StatementError};
6
7/// A value a statement binds, named by its meaning; the broker supplies it when the statement runs.
8///
9/// # Examples
10///
11/// ```
12/// # use std::num::NonZeroUsize;
13/// use ruststream_sqlx_dialect::{Dialect, Param, Statement, StatementError, TableSpec};
14/// # use ruststream_sqlx_dialect::TableName;
15///
16/// /// SQL Server, a database without a built-in dialect.
17/// #[derive(Debug)]
18/// pub struct Mssql;
19///
20/// impl Dialect for Mssql {
21///     fn name(&self) -> &'static str {
22///         "mssql"
23///     }
24///
25///     // A delayed retry of a leased email names the row and the lease its delivery holds; the
26///     // broker binds the delay's end, the id and the lease, in this order.
27///     fn retry_after(&self, _spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
28///         Ok(Statement::new(
29///             "UPDATE [email_jobs] SET [retry_after] = @p1, [locked_until] = NULL \
30///              WHERE [job_id] = @p2 AND [locked_until] = @p3",
31///             [Param::RetryAfter, Param::Id, Param::Held],
32///         ))
33///     }
34/// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
35/// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
36/// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
37/// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
38/// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
39/// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
40/// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
41/// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
42/// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
43/// }
44/// ```
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46#[non_exhaustive]
47pub enum Param {
48    /// The current time, in the type of the column it meets.
49    Now,
50    /// The group the subscription reads.
51    Group,
52    /// The most rows one claim takes.
53    Limit,
54    /// The id of the row being settled.
55    Id,
56    /// The ids a fetch assembles, as one list.
57    Ids,
58    /// The time before which a row is not claimed again.
59    RetryAfter,
60    /// Where a dead-lettered row goes: the name of its new group.
61    Destination,
62    /// The delay of a retry in microseconds, for a statement that adds it to the database's own
63    /// time.
64    Delay,
65    /// The expiry a claim, a stamp or an extension writes into the lease column: the row stays
66    /// with its delivery until then.
67    Lease,
68    /// The current time, in the type of the lease column: a row whose lease ended by then is
69    /// claimable.
70    LeaseNow,
71    /// The expiry the delivery's claim or its last extension wrote: the ownership token that
72    /// settlement and extension match.
73    Held,
74    /// The lock key of the row, as text: what the advisory claim selected as `__lock`, which the
75    /// lock and the unlock name.
76    Key,
77    /// The value of the column at this position of [`TableSpec::columns`](crate::TableSpec::columns),
78    /// for an insert.
79    Column(usize),
80}
81
82/// One SQL statement: its text and the parameters its placeholders bind, in placeholder order.
83///
84/// # Examples
85///
86/// ```
87/// # use std::num::NonZeroUsize;
88/// use ruststream_sqlx_dialect::{Dialect, Param, Statement, StatementError, TableSpec};
89/// # use ruststream_sqlx_dialect::TableName;
90///
91/// /// SQL Server, a database without a built-in dialect.
92/// #[derive(Debug)]
93/// pub struct Mssql;
94///
95/// impl Dialect for Mssql {
96///     fn name(&self) -> &'static str {
97///         "mssql"
98///     }
99///
100///     // A dropped email stays in its table, marked with the time it was dropped.
101///     fn discard(&self, _spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
102///         Ok(Statement::new(
103///             "UPDATE [email_jobs] SET [dropped_at] = @p1 WHERE [job_id] = @p2",
104///             [Param::Now, Param::Id],
105///         ))
106///     }
107/// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
108/// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
109/// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
110/// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
111/// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
112/// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
113/// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
114/// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
115/// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
116/// }
117/// ```
118#[derive(Debug, Clone, PartialEq, Eq, Hash)]
119pub struct Statement {
120    sql: String,
121    params: Vec<Param>,
122}
123
124impl Statement {
125    /// A statement with its text and the parameters its placeholders bind, in order.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// # use std::num::NonZeroUsize;
131    /// use ruststream_sqlx_dialect::{
132    ///     ClaimShape, Dialect, Param, RowLock, Statement, StatementError, TableSpec,
133    /// };
134    /// # use ruststream_sqlx_dialect::TableName;
135    ///
136    /// /// SQL Server, a database without a built-in dialect.
137    /// #[derive(Debug)]
138    /// pub struct Mssql;
139    ///
140    /// impl RowLock for Mssql {
141    ///     // The claim of the service's one queue table, which it reads whole.
142    ///     fn lock_claim(
143    ///         &self,
144    ///         spec: &TableSpec<'_>,
145    ///         _shape: ClaimShape,
146    ///     ) -> Result<Statement, StatementError> {
147    ///         // This claim keeps no group in order.
148    ///         if spec.is_fifo() {
149    ///             return Err(StatementError::UnsupportedFifo { dialect: self.name() });
150    ///         }
151    ///         Ok(Statement::new(
152    ///             "SELECT TOP (@p1) [job_id], [payload] FROM [email_jobs] \
153    ///              WITH (UPDLOCK, READPAST) ORDER BY [job_id]",
154    ///             [Param::Limit],
155    ///         ))
156    ///     }
157    /// }
158    /// # impl Dialect for Mssql {
159    /// #     fn name(&self) -> &'static str { "mssql" }
160    /// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
161    /// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
162    /// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
163    /// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
164    /// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
165    /// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
166    /// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
167    /// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
168    /// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
169    /// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
170    /// # }
171    /// ```
172    #[must_use]
173    pub fn new(sql: impl Into<String>, params: impl IntoIterator<Item = Param>) -> Self {
174        Self {
175            sql: sql.into(),
176            params: params.into_iter().collect(),
177        }
178    }
179
180    /// The statement's text.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// # use std::num::NonZeroUsize;
186    /// # use ruststream_sqlx_dialect::TableName;
187    /// use ruststream_sqlx_dialect::{
188    ///     ClaimShape, Dialect, Lease, Param, Statement, StatementError, TableSpec,
189    /// };
190    ///
191    /// /// SQL Server, a database without a built-in dialect.
192    /// #[derive(Debug)]
193    /// pub struct Mssql;
194    ///
195    /// impl Mssql {
196    ///     // The claimable rows of `email_jobs`, which both the lease claim and a claim of the
197    ///     // service's own (`custom(claim)`) read.
198    ///     fn candidates(&self) -> Statement {
199    ///         Statement::new(
200    ///             "SELECT TOP (@p2) * FROM [email_jobs] WITH (UPDLOCK, READPAST, ROWLOCK) \
201    ///              WHERE [locked_until] IS NULL OR [locked_until] <= @p1 ORDER BY [job_id]",
202    ///             [Param::LeaseNow, Param::Limit],
203    ///         )
204    ///     }
205    /// }
206    ///
207    /// impl Lease for Mssql {
208    ///     // The lease claim updates the candidates in place: their text becomes the claim's
209    ///     // common table expression.
210    ///     fn lease_claim(
211    ///         &self,
212    ///         _spec: &TableSpec<'_>,
213    ///         _shape: ClaimShape,
214    ///     ) -> Result<Statement, StatementError> {
215    ///         let candidates = self.candidates();
216    ///         Ok(Statement::new(
217    ///             format!(
218    ///                 "WITH [claimed] AS ({}) UPDATE [claimed] \
219    ///                  SET [locked_until] = @p3, [attempt] = [attempt] + 1 \
220    ///                  OUTPUT deleted.[job_id], deleted.[attempt], deleted.[payload]",
221    ///                 candidates.sql()
222    ///             ),
223    ///             [Param::LeaseNow, Param::Limit, Param::Lease],
224    ///         ))
225    ///     }
226    /// #     fn extend(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
227    /// #     fn stamp(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
228    /// }
229    /// # impl Dialect for Mssql {
230    /// #     fn name(&self) -> &'static str { "mssql" }
231    /// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
232    /// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
233    /// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
234    /// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
235    /// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
236    /// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
237    /// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
238    /// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
239    /// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
240    /// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
241    /// # }
242    /// ```
243    #[must_use]
244    pub fn sql(&self) -> &str {
245        &self.sql
246    }
247
248    /// The parameters the placeholders bind, in placeholder order.
249    ///
250    /// # Examples
251    ///
252    /// ```
253    /// # use std::num::NonZeroUsize;
254    /// # use ruststream_sqlx_dialect::TableName;
255    /// use ruststream_sqlx_dialect::{
256    ///     ClaimShape, Dialect, Lease, Param, Statement, StatementError, TableSpec,
257    /// };
258    ///
259    /// /// SQL Server, a database without a built-in dialect.
260    /// #[derive(Debug)]
261    /// pub struct Mssql;
262    ///
263    /// impl Mssql {
264    ///     // The claimable rows of `email_jobs`, which both the lease claim and a claim of the
265    ///     // service's own (`custom(claim)`) read.
266    ///     fn candidates(&self) -> Statement {
267    ///         Statement::new(
268    ///             "SELECT TOP (@p2) * FROM [email_jobs] WITH (UPDLOCK, READPAST, ROWLOCK) \
269    ///              WHERE [locked_until] IS NULL OR [locked_until] <= @p1 ORDER BY [job_id]",
270    ///             [Param::LeaseNow, Param::Limit],
271    ///         )
272    ///     }
273    /// }
274    ///
275    /// impl Lease for Mssql {
276    ///     // The lease claim updates the candidates in place. Its parameters are theirs, then the
277    ///     // lease it writes, so `@p3` binds the lease.
278    ///     fn lease_claim(
279    ///         &self,
280    ///         _spec: &TableSpec<'_>,
281    ///         _shape: ClaimShape,
282    ///     ) -> Result<Statement, StatementError> {
283    ///         let candidates = self.candidates();
284    ///         Ok(Statement::new(
285    ///             format!(
286    ///                 "WITH [claimed] AS ({}) UPDATE [claimed] \
287    ///                  SET [locked_until] = @p3, [attempt] = [attempt] + 1 \
288    ///                  OUTPUT deleted.[job_id], deleted.[attempt], deleted.[payload]",
289    ///                 candidates.sql()
290    ///             ),
291    ///             candidates.params().iter().copied().chain([Param::Lease]),
292    ///         ))
293    ///     }
294    /// #     fn extend(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
295    /// #     fn stamp(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
296    /// }
297    /// # impl Dialect for Mssql {
298    /// #     fn name(&self) -> &'static str { "mssql" }
299    /// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
300    /// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
301    /// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
302    /// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
303    /// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
304    /// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
305    /// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
306    /// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
307    /// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
308    /// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
309    /// # }
310    /// ```
311    #[must_use]
312    pub fn params(&self) -> &[Param] {
313        &self.params
314    }
315}
316
317/// What a claim selects: whole rows, only their ids for a service's own fetch, or the columns
318/// that run the queue, each named by its role, for a reader that knows no struct.
319///
320/// # Examples
321///
322/// A claim of a dialect of the service's own selects what the shape asks for:
323///
324/// ```
325/// # use std::num::NonZeroUsize;
326/// use ruststream_sqlx_dialect::{
327///     ClaimShape, Dialect, Param, Role, RowLock, Statement, StatementError, TableSpec,
328/// };
329/// # use ruststream_sqlx_dialect::TableName;
330///
331/// /// SQL Server, a database without a built-in dialect.
332/// #[derive(Debug)]
333/// pub struct Mssql;
334///
335/// impl RowLock for Mssql {
336///     // The service's queue tables have no groups and no claim order of their own.
337///     fn lock_claim(
338///         &self,
339///         spec: &TableSpec<'_>,
340///         shape: ClaimShape,
341///     ) -> Result<Statement, StatementError> {
342///         let mut selected = String::new();
343///         match shape {
344///             // Whole rows, for a struct the broker reads.
345///             ClaimShape::Rows => {
346///                 for column in spec.columns() {
347///                     if !selected.is_empty() {
348///                         selected.push_str(", ");
349///                     }
350///                     self.quote_into(column.name(), &mut selected);
351///                 }
352///             }
353///             // Only the ids, for a struct that fetches its rows itself.
354///             ClaimShape::Ids => self.quote_into(spec.id().name(), &mut selected),
355///             // The columns that run the queue, each under the name of its role.
356///             ClaimShape::Roles => {
357///                 let roles = [
358///                     Role::Id,
359///                     Role::PartitionKey,
360///                     Role::Attempt,
361///                     Role::Headers,
362///                     Role::Payload,
363///                 ];
364///                 for role in roles {
365///                     let Some(column) = spec.column(role) else {
366///                         continue;
367///                     };
368///                     if !selected.is_empty() {
369///                         selected.push_str(", ");
370///                     }
371///                     self.quote_into(column.name(), &mut selected);
372///                     selected.push_str(" AS ");
373///                     self.quote_into(role.attribute(), &mut selected);
374///                 }
375///             }
376///         }
377///         let mut sql = format!("SELECT TOP (@p1) {selected} FROM ");
378///         self.quote_into(spec.table(), &mut sql);
379///         sql.push_str(" WITH (UPDLOCK, READPAST) ORDER BY ");
380///         self.quote_into(spec.id().name(), &mut sql);
381///         Ok(Statement::new(sql, [Param::Limit]))
382///     }
383/// }
384/// # impl Dialect for Mssql {
385/// #     fn name(&self) -> &'static str { "mssql" }
386/// #     fn quote_into(&self, ident: &str, out: &mut String) { out.push('['); out.push_str(&ident.replace(']', "]]")); out.push(']'); }
387/// #     fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) { out.push_str("@p"); out.push_str(&index.to_string()); }
388/// #     fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedFetch { dialect: self.name() }) }
389/// #     fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
390/// #     fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
391/// #     fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
392/// #     fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
393/// #     fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
394/// #     fn dead_letter_table(&self, spec: &TableSpec<'_>, target: TableName<'_>) -> Result<Vec<Statement>, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
395/// #     fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> { Err(StatementError::UnsupportedForm { dialect: self.name(), form: spec.form().name() }) }
396/// # }
397/// ```
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
399pub enum ClaimShape {
400    /// Every column of the claimed rows.
401    Rows,
402    /// Only the id of each claimed row.
403    Ids,
404    /// Only the columns that play `id`, `partition_key`, `attempt`, `headers` and `payload`,
405    /// each under its role's attribute name as an alias, in [`Role::ALL`](crate::Role::ALL)
406    /// order.
407    Roles,
408}
409
410#[cfg(test)]
411mod tests {
412    use super::{Param, Statement};
413
414    #[test]
415    fn a_statement_keeps_its_text_and_parameters_in_order() {
416        let statement = Statement::new(
417            "UPDATE jobs SET processed_at = $1 WHERE job_id = $2",
418            [Param::Now, Param::Id],
419        );
420        assert_eq!(
421            statement.sql(),
422            "UPDATE jobs SET processed_at = $1 WHERE job_id = $2"
423        );
424        assert_eq!(statement.params(), [Param::Now, Param::Id]);
425    }
426}