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}