Skip to main content

ruststream_sqlx_dialect/spec/
builder.rs

1//! How a description is built: the constructor and one setter per role and setting, each filling
2//! its slot once.
3
4use super::{Grouping, TableSpec};
5use crate::column::Column;
6use crate::form::Form;
7use crate::opening::{Isolation, Mode, Opening};
8
9impl<'a> TableSpec<'a> {
10    /// A table in the connection's default schema, with the column that identifies a row and the
11    /// form its rows are claimed in.
12    ///
13    /// `#[derive(Inbox)]` builds the description from `#[inbox(table = "..")]`, the `#[field(id)]`
14    /// field and the form the struct declares.
15    ///
16    /// # Examples
17    ///
18    /// ```
19    /// # #[cfg(feature = "postgres")]
20    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
21    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Postgres, TableSpec};
22    ///
23    /// // The service's email queue, its rows taken by row lock.
24    /// const EMAILS: TableSpec<'static> =
25    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
26    ///         .payload(Column::new("payload"));
27    ///
28    /// // A row the handler acknowledged leaves the table.
29    /// let ack = Postgres.ack(&EMAILS)?;
30    /// assert_eq!(ack.sql(), r#"DELETE FROM "email_jobs" WHERE "job_id" = $1"#);
31    /// # Ok(())
32    /// # }
33    /// # #[cfg(not(feature = "postgres"))]
34    /// # fn main() {}
35    /// ```
36    #[must_use]
37    pub const fn new(table: &'a str, id: Column<'a>, form: Form<'a>) -> Self {
38        Self {
39            schema: None,
40            table,
41            id,
42            grouping: Grouping::None,
43            partition_key: None,
44            priority: None,
45            retry_after: None,
46            attempt: None,
47            processed_at: None,
48            headers: None,
49            payload: None,
50            data: &[],
51            fetched: &[],
52            form,
53            opening: Opening::Default,
54            select_all: false,
55            database_clock: false,
56        }
57    }
58
59    /// The same table, inside `schema`.
60    ///
61    /// `#[inbox(schema = "app")]` sets it.
62    ///
63    /// # Panics
64    ///
65    /// When the description already names a schema. In a `const` the panic is a build error.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// # #[cfg(feature = "postgres")]
71    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
72    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Postgres, TableSpec};
73    ///
74    /// // The queue lives in the `app` schema, and every statement names it.
75    /// const EMAILS: TableSpec<'static> =
76    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
77    ///         .within("app")
78    ///         .payload(Column::new("payload"));
79    ///
80    /// let ack = Postgres.ack(&EMAILS)?;
81    /// assert_eq!(ack.sql(), r#"DELETE FROM "app"."email_jobs" WHERE "job_id" = $1"#);
82    /// # Ok(())
83    /// # }
84    /// # #[cfg(not(feature = "postgres"))]
85    /// # fn main() {}
86    /// ```
87    #[must_use]
88    pub const fn within(self, schema: &'a str) -> Self {
89        assert!(
90            self.schema.is_none(),
91            "a table description is given the schema twice: name it once"
92        );
93        Self {
94            schema: Some(schema),
95            ..self
96        }
97    }
98
99    /// The same table, split into groups by `column`; a subscription reads one group.
100    ///
101    /// `#[field(group)]` on a field sets it.
102    ///
103    /// # Panics
104    ///
105    /// When the description already splits into groups, by this setter or by
106    /// [`fifo_group`](Self::fifo_group). In a `const` the panic is a build error.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// # #[cfg(feature = "postgres")]
112    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
113    /// use ruststream_sqlx_dialect::{
114    ///     ClaimShape, Column, Form, Param, Postgres, RowLock, TableSpec,
115    /// };
116    ///
117    /// // One table serves several queues; a subscription binds its queue's name.
118    /// const EMAILS: TableSpec<'static> =
119    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
120    ///         .group(Column::new("queue"))
121    ///         .payload(Column::new("payload"));
122    ///
123    /// let claim = Postgres.lock_claim(&EMAILS, ClaimShape::Ids)?;
124    /// assert_eq!(
125    ///     claim.sql(),
126    ///     r#"SELECT "job_id" FROM "email_jobs" WHERE "queue" = $1 ORDER BY "job_id" LIMIT $2 FOR UPDATE SKIP LOCKED"#
127    /// );
128    /// assert_eq!(claim.params(), [Param::Group, Param::Limit]);
129    /// # Ok(())
130    /// # }
131    /// # #[cfg(not(feature = "postgres"))]
132    /// # fn main() {}
133    /// ```
134    #[must_use]
135    pub const fn group(self, column: Column<'a>) -> Self {
136        assert!(
137            matches!(self.grouping, Grouping::None),
138            "a table description sets its `group` column twice (`group` or `fifo_group`): a role \
139             belongs to one column"
140        );
141        Self {
142            grouping: Grouping::Groups(column),
143            ..self
144        }
145    }
146
147    /// The same table, split into groups by `column`, with each group in order: at most one row of
148    /// a group is in work, taken in claim order.
149    ///
150    /// A claim takes the group's head, its first unfinished row in claim order, and takes nothing
151    /// while a row of the group is in work or the head is not yet due: the claim's transaction
152    /// first takes the group ([`Dialect::fifo_guard`](crate::Dialect::fifo_guard)), and a lease
153    /// claim waits while a row of the group holds a lease. A delayed retry gives its row a later
154    /// `retry_after`, so the row moves behind the rows of its group due earlier.
155    ///
156    /// The advisory lock form keeps a group in order through its lock key instead, so its claim
157    /// and its take refuse a FIFO group
158    /// ([`StatementError::AdvisoryFifo`](crate::StatementError::AdvisoryFifo)).
159    ///
160    /// `#[field(group, fifo = true)]` on a field sets it.
161    ///
162    /// # Panics
163    ///
164    /// When the description already splits into groups, by this setter or by
165    /// [`group`](Self::group). In a `const` the panic is a build error.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// # #[cfg(feature = "postgres")]
171    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
172    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Param, Postgres, TableSpec};
173    ///
174    /// // The emails of one customer go out in order.
175    /// const EMAILS: TableSpec<'static> =
176    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
177    ///         .fifo_group(Column::new("customer"))
178    ///         .payload(Column::new("payload"));
179    ///
180    /// // The claim's transaction first takes the customer's group.
181    /// let guard = Postgres.fifo_guard(&EMAILS)?;
182    /// assert_eq!(guard.map(|guard| guard.params().to_vec()), Some(vec![Param::Group]));
183    /// # Ok(())
184    /// # }
185    /// # #[cfg(not(feature = "postgres"))]
186    /// # fn main() {}
187    /// ```
188    #[must_use]
189    pub const fn fifo_group(self, column: Column<'a>) -> Self {
190        assert!(
191            matches!(self.grouping, Grouping::None),
192            "a table description sets its `group` column twice (`group` or `fifo_group`): a role \
193             belongs to one column"
194        );
195        Self {
196            grouping: Grouping::Fifo(column),
197            ..self
198        }
199    }
200
201    /// The same table, with the delivery's partition key read from `column`.
202    ///
203    /// `#[field(partition_key)]` on a field sets it.
204    ///
205    /// # Panics
206    ///
207    /// When the description already has a `partition_key` column: a role belongs to one column. In a
208    /// `const` the panic is a build error.
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// # #[cfg(feature = "postgres")]
214    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
215    /// use ruststream_sqlx_dialect::{ClaimShape, Column, Form, Postgres, RowLock, TableSpec};
216    ///
217    /// // The emails of one tenant run in one lane of `workers(n, by_key)`.
218    /// const EMAILS: TableSpec<'static> =
219    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
220    ///         .partition_key(Column::new("tenant"))
221    ///         .payload(Column::new("payload"));
222    ///
223    /// let claim = Postgres.lock_claim(&EMAILS, ClaimShape::Roles)?;
224    /// assert_eq!(
225    ///     claim.sql(),
226    ///     r#"SELECT "job_id" AS "id", "tenant" AS "partition_key", "payload" AS "payload" FROM "email_jobs" ORDER BY "job_id" LIMIT $1 FOR UPDATE SKIP LOCKED"#
227    /// );
228    /// # Ok(())
229    /// # }
230    /// # #[cfg(not(feature = "postgres"))]
231    /// # fn main() {}
232    /// ```
233    #[must_use]
234    pub const fn partition_key(self, column: Column<'a>) -> Self {
235        assert!(
236            self.partition_key.is_none(),
237            "a table description sets its `partition_key` column twice: a role belongs to one column"
238        );
239        Self {
240            partition_key: Some(column),
241            ..self
242        }
243    }
244
245    /// The same table, with rows claimed in the order of `column`, a smaller value first.
246    ///
247    /// `#[field(priority)]` on a field sets it.
248    ///
249    /// # Panics
250    ///
251    /// When the description already has a `priority` column: a role belongs to one column. In a
252    /// `const` the panic is a build error.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// # #[cfg(feature = "postgres")]
258    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
259    /// use ruststream_sqlx_dialect::{ClaimShape, Column, Form, Postgres, RowLock, TableSpec};
260    ///
261    /// // An email with a smaller `priority` goes out first.
262    /// const EMAILS: TableSpec<'static> =
263    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
264    ///         .priority(Column::new("priority"))
265    ///         .payload(Column::new("payload"));
266    ///
267    /// let claim = Postgres.lock_claim(&EMAILS, ClaimShape::Ids)?;
268    /// assert_eq!(
269    ///     claim.sql(),
270    ///     r#"SELECT "job_id" FROM "email_jobs" ORDER BY "priority", "job_id" LIMIT $1 FOR UPDATE SKIP LOCKED"#
271    /// );
272    /// # Ok(())
273    /// # }
274    /// # #[cfg(not(feature = "postgres"))]
275    /// # fn main() {}
276    /// ```
277    #[must_use]
278    pub const fn priority(self, column: Column<'a>) -> Self {
279        assert!(
280            self.priority.is_none(),
281            "a table description sets its `priority` column twice: a role belongs to one column"
282        );
283        Self {
284            priority: Some(column),
285            ..self
286        }
287    }
288
289    /// The same table, where `column` holds the time before which a row is not claimed.
290    ///
291    /// `#[field(retry_after)]` on a field sets it.
292    ///
293    /// # Panics
294    ///
295    /// When the description already has a `retry_after` column: a role belongs to one column. In a
296    /// `const` the panic is a build error.
297    ///
298    /// # Examples
299    ///
300    /// ```
301    /// # #[cfg(feature = "postgres")]
302    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
303    /// use ruststream_sqlx_dialect::{
304    ///     ClaimShape, Column, Form, Param, Postgres, RowLock, TableSpec,
305    /// };
306    ///
307    /// // An email waits in the table until its `run_at`.
308    /// const EMAILS: TableSpec<'static> =
309    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
310    ///         .retry_after(Column::new("run_at"))
311    ///         .payload(Column::new("payload"));
312    ///
313    /// let claim = Postgres.lock_claim(&EMAILS, ClaimShape::Ids)?;
314    /// assert_eq!(
315    ///     claim.sql(),
316    ///     r#"SELECT "job_id" FROM "email_jobs" WHERE "run_at" <= $1 ORDER BY "run_at", "job_id" LIMIT $2 FOR UPDATE SKIP LOCKED"#
317    /// );
318    /// assert_eq!(claim.params(), [Param::Now, Param::Limit]);
319    /// # Ok(())
320    /// # }
321    /// # #[cfg(not(feature = "postgres"))]
322    /// # fn main() {}
323    /// ```
324    #[must_use]
325    pub const fn retry_after(self, column: Column<'a>) -> Self {
326        assert!(
327            self.retry_after.is_none(),
328            "a table description sets its `retry_after` column twice: a role belongs to one column"
329        );
330        Self {
331            retry_after: Some(column),
332            ..self
333        }
334    }
335
336    /// The same table, where `column` counts the attempts.
337    ///
338    /// `#[field(attempt)]` on a field sets it.
339    ///
340    /// # Panics
341    ///
342    /// When the description already has a `attempt` column: a role belongs to one column. In a
343    /// `const` the panic is a build error.
344    ///
345    /// # Examples
346    ///
347    /// ```
348    /// # #[cfg(feature = "postgres")]
349    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
350    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Postgres, TableSpec};
351    ///
352    /// // A retried email counts one more attempt.
353    /// const EMAILS: TableSpec<'static> =
354    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
355    ///         .attempt(Column::new("attempt"))
356    ///         .payload(Column::new("payload"));
357    ///
358    /// let retry = Postgres.retry(&EMAILS)?.map(|retry| retry.sql().to_owned());
359    /// assert_eq!(
360    ///     retry.as_deref(),
361    ///     Some(r#"UPDATE "email_jobs" SET "attempt" = "attempt" + 1 WHERE "job_id" = $1"#),
362    /// );
363    /// # Ok(())
364    /// # }
365    /// # #[cfg(not(feature = "postgres"))]
366    /// # fn main() {}
367    /// ```
368    #[must_use]
369    pub const fn attempt(self, column: Column<'a>) -> Self {
370        assert!(
371            self.attempt.is_none(),
372            "a table description sets its `attempt` column twice: a role belongs to one column"
373        );
374        Self {
375            attempt: Some(column),
376            ..self
377        }
378    }
379
380    /// The same table, where acknowledgement sets `column` instead of deleting the row.
381    ///
382    /// `#[field(processed_at)]` on a field sets it.
383    ///
384    /// # Panics
385    ///
386    /// When the description already has a `processed_at` column: a role belongs to one column. In a
387    /// `const` the panic is a build error.
388    ///
389    /// # Examples
390    ///
391    /// ```
392    /// # #[cfg(feature = "postgres")]
393    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
394    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Param, Postgres, TableSpec};
395    ///
396    /// // A sent email stays in the table, stamped with the time it was sent.
397    /// const EMAILS: TableSpec<'static> =
398    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
399    ///         .processed_at(Column::new("sent_at"))
400    ///         .payload(Column::new("payload"));
401    ///
402    /// let ack = Postgres.ack(&EMAILS)?;
403    /// assert_eq!(ack.sql(), r#"UPDATE "email_jobs" SET "sent_at" = $1 WHERE "job_id" = $2"#);
404    /// assert_eq!(ack.params(), [Param::Now, Param::Id]);
405    /// # Ok(())
406    /// # }
407    /// # #[cfg(not(feature = "postgres"))]
408    /// # fn main() {}
409    /// ```
410    #[must_use]
411    pub const fn processed_at(self, column: Column<'a>) -> Self {
412        assert!(
413            self.processed_at.is_none(),
414            "a table description sets its `processed_at` column twice: a role belongs to one column"
415        );
416        Self {
417            processed_at: Some(column),
418            ..self
419        }
420    }
421
422    /// The same table, where `column` holds the delivery's headers.
423    ///
424    /// `#[field(headers)]` on a field sets it.
425    ///
426    /// # Panics
427    ///
428    /// When the description already has a `headers` column: a role belongs to one column. In a
429    /// `const` the panic is a build error.
430    ///
431    /// # Examples
432    ///
433    /// ```
434    /// # #[cfg(feature = "postgres")]
435    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
436    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Postgres, TableSpec};
437    ///
438    /// // An email carries its headers in one column, which the insert writes.
439    /// const EMAILS: TableSpec<'static> =
440    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
441    ///         .headers(Column::new("headers"))
442    ///         .payload(Column::new("payload"));
443    ///
444    /// let insert = Postgres.insert(&EMAILS)?;
445    /// assert_eq!(
446    ///     insert.sql(),
447    ///     r#"INSERT INTO "email_jobs" ("job_id", "headers", "payload") VALUES ($1, $2, $3)"#
448    /// );
449    /// # Ok(())
450    /// # }
451    /// # #[cfg(not(feature = "postgres"))]
452    /// # fn main() {}
453    /// ```
454    #[must_use]
455    pub const fn headers(self, column: Column<'a>) -> Self {
456        assert!(
457            self.headers.is_none(),
458            "a table description sets its `headers` column twice: a role belongs to one column"
459        );
460        Self {
461            headers: Some(column),
462            ..self
463        }
464    }
465
466    /// The same table, where `column` holds the message bytes a handler decodes.
467    ///
468    /// `#[field(payload)]` on a field sets it.
469    ///
470    /// # Panics
471    ///
472    /// When the description already has a `payload` column: a role belongs to one column. In a
473    /// `const` the panic is a build error.
474    ///
475    /// ```compile_fail,E0080
476    /// use ruststream_sqlx_dialect::{Column, Form, TableSpec};
477    ///
478    /// // Two columns for one role do not build.
479    /// const JOBS: TableSpec<'static> = TableSpec::new("jobs", Column::new("id"), Form::RowLock)
480    ///     .payload(Column::new("payload"))
481    ///     .payload(Column::new("body"));
482    /// # fn main() { let _ = JOBS; }
483    /// ```
484    ///
485    /// # Examples
486    ///
487    /// ```
488    /// # #[cfg(feature = "postgres")]
489    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
490    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Postgres, TableSpec};
491    ///
492    /// // The handler decodes the email from `payload`, which the fetch reads.
493    /// const EMAILS: TableSpec<'static> =
494    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
495    ///         .payload(Column::new("payload"));
496    ///
497    /// let fetch = Postgres.fetch(&EMAILS)?;
498    /// assert_eq!(
499    ///     fetch.sql(),
500    ///     r#"SELECT "job_id", "payload" FROM "email_jobs" WHERE "job_id" = ANY($1)"#
501    /// );
502    /// # Ok(())
503    /// # }
504    /// # #[cfg(not(feature = "postgres"))]
505    /// # fn main() {}
506    /// ```
507    #[must_use]
508    pub const fn payload(self, column: Column<'a>) -> Self {
509        assert!(
510            self.payload.is_none(),
511            "a table description sets its `payload` column twice: a role belongs to one column"
512        );
513        Self {
514            payload: Some(column),
515            ..self
516        }
517    }
518
519    /// The same table, with the columns of the message's own data: the columns without a role.
520    ///
521    /// `#[derive(Inbox)]` passes every field without a role here.
522    ///
523    /// # Panics
524    ///
525    /// When the description already lists data columns: the list is given once. In a `const` the
526    /// panic is a build error.
527    ///
528    /// # Examples
529    ///
530    /// ```
531    /// # #[cfg(feature = "postgres")]
532    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
533    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Postgres, TableSpec};
534    ///
535    /// // The email's recipient and subject are columns of their own, after the roles.
536    /// const EMAILS: TableSpec<'static> =
537    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
538    ///         .payload(Column::new("payload"))
539    ///         .data(&[Column::new("recipient"), Column::new("subject")]);
540    ///
541    /// let insert = Postgres.insert(&EMAILS)?;
542    /// assert_eq!(
543    ///     insert.sql(),
544    ///     r#"INSERT INTO "email_jobs" ("job_id", "payload", "recipient", "subject") VALUES ($1, $2, $3, $4)"#
545    /// );
546    /// # Ok(())
547    /// # }
548    /// # #[cfg(not(feature = "postgres"))]
549    /// # fn main() {}
550    /// ```
551    #[must_use]
552    pub const fn data(self, columns: &'a [Column<'a>]) -> Self {
553        assert!(
554            self.data.is_empty(),
555            "a table description lists its data columns twice: give the list once"
556        );
557        Self {
558            data: columns,
559            ..self
560        }
561    }
562
563    /// The same table, with the columns a message assembled from it reads beside the headers
564    /// struct's: listed after the data, wherever the statements list the table's columns.
565    ///
566    /// `#[derive(Inbox)]` passes the message struct's own fields here when the struct flattens a
567    /// headers struct into its `#[field(headers)]` field, which describes the rest of the table.
568    /// The default fetch then names every column it reads, so a column the table lacks stops the
569    /// subscription when its statements are prepared.
570    ///
571    /// # Panics
572    ///
573    /// When the description already lists fetched columns: the list is given once. In a `const`
574    /// the panic is a build error.
575    ///
576    /// # Examples
577    ///
578    /// ```
579    /// # #[cfg(feature = "postgres")]
580    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
581    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Postgres, TableSpec};
582    ///
583    /// // The message flattens a headers struct into `headers` and reads `recipient` itself.
584    /// const EMAILS: TableSpec<'static> =
585    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
586    ///         .headers(Column::new("headers"))
587    ///         .payload(Column::new("payload"))
588    ///         .fetching(&[Column::new("recipient")]);
589    ///
590    /// let fetch = Postgres.fetch(&EMAILS)?;
591    /// assert_eq!(
592    ///     fetch.sql(),
593    ///     r#"SELECT "job_id", "headers", "payload", "recipient" FROM "email_jobs" WHERE "job_id" = ANY($1)"#
594    /// );
595    /// # Ok(())
596    /// # }
597    /// # #[cfg(not(feature = "postgres"))]
598    /// # fn main() {}
599    /// ```
600    #[must_use]
601    pub const fn fetching(self, columns: &'a [Column<'a>]) -> Self {
602        assert!(
603            self.fetched.is_empty(),
604            "a table description lists its fetched columns twice: give the list once"
605        );
606        Self {
607            fetched: columns,
608            ..self
609        }
610    }
611
612    /// The same table, read with `*`: the struct flattens another, so the columns are not all
613    /// known.
614    ///
615    /// A dead-letter move to another table then copies the row by position, so that table has the
616    /// same columns in the same order.
617    ///
618    /// A struct with a `#[sqlx(flatten)]` field sets it.
619    ///
620    /// # Examples
621    ///
622    /// ```
623    /// # #[cfg(feature = "postgres")]
624    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
625    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Postgres, TableSpec};
626    ///
627    /// // The email struct flattens another, so the fetch reads every column.
628    /// const EMAILS: TableSpec<'static> =
629    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
630    ///         .payload(Column::new("payload"))
631    ///         .selecting_all();
632    ///
633    /// let fetch = Postgres.fetch(&EMAILS)?;
634    /// assert_eq!(fetch.sql(), r#"SELECT * FROM "email_jobs" WHERE "job_id" = ANY($1)"#);
635    /// # Ok(())
636    /// # }
637    /// # #[cfg(not(feature = "postgres"))]
638    /// # fn main() {}
639    /// ```
640    #[must_use]
641    pub const fn selecting_all(self) -> Self {
642        Self {
643            select_all: true,
644            ..self
645        }
646    }
647
648    /// Makes the statements read the database's own clock instead of a time the service binds.
649    ///
650    /// `#[inbox(clock = DatabaseClock)]` sets it.
651    ///
652    /// # Examples
653    ///
654    /// ```
655    /// # #[cfg(feature = "postgres")]
656    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
657    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Param, Postgres, TableSpec};
658    ///
659    /// // A sent email is stamped by the database's clock, so the service binds no time.
660    /// const EMAILS: TableSpec<'static> =
661    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
662    ///         .processed_at(Column::new("sent_at"))
663    ///         .payload(Column::new("payload"))
664    ///         .database_clock();
665    ///
666    /// let ack = Postgres.ack(&EMAILS)?;
667    /// assert_eq!(
668    ///     ack.sql(),
669    ///     r#"UPDATE "email_jobs" SET "sent_at" = statement_timestamp() WHERE "job_id" = $1"#
670    /// );
671    /// assert_eq!(ack.params(), [Param::Id]);
672    /// # Ok(())
673    /// # }
674    /// # #[cfg(not(feature = "postgres"))]
675    /// # fn main() {}
676    /// ```
677    #[must_use]
678    pub const fn database_clock(self) -> Self {
679        Self {
680            database_clock: true,
681            ..self
682        }
683    }
684
685    /// The same table, with its transactions opened at the isolation level `isolation`.
686    ///
687    /// The row lock claim's transaction opens at it, and so does the transaction a broker opens
688    /// for a handler's writes. A table opens at one level or in one mode.
689    ///
690    /// `#[inbox(isolation = repeatable_read)]` sets it.
691    ///
692    /// # Panics
693    ///
694    /// When the description already opens at a level or in a mode, by this setter or by
695    /// [`mode`](Self::mode). In a `const` the panic is a build error.
696    ///
697    /// # Examples
698    ///
699    /// ```
700    /// # #[cfg(feature = "postgres")]
701    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
702    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Isolation, Postgres, TableSpec};
703    ///
704    /// // The handler's reads of other tables see one snapshot.
705    /// const EMAILS: TableSpec<'static> =
706    ///     TableSpec::new("email_jobs", Column::new("job_id"), Form::RowLock)
707    ///         .payload(Column::new("payload"))
708    ///         .isolation(Isolation::RepeatableRead);
709    ///
710    /// let begin = Postgres.begin(EMAILS.opening())?;
711    /// assert_eq!(begin, Some("BEGIN ISOLATION LEVEL REPEATABLE READ"));
712    /// # Ok(())
713    /// # }
714    /// # #[cfg(not(feature = "postgres"))]
715    /// # fn main() {}
716    /// ```
717    #[must_use]
718    pub const fn isolation(self, isolation: Isolation) -> Self {
719        assert!(
720            matches!(self.opening, Opening::Default),
721            "a table description sets its opening twice (`isolation` or `mode`): a table opens at \
722             one level or in one mode"
723        );
724        Self {
725            opening: Opening::Isolation(isolation),
726            ..self
727        }
728    }
729
730    /// The same table, with its SQLite transactions opened in `mode`.
731    ///
732    /// A table opens in one mode or at one level.
733    ///
734    /// `#[inbox(mode = immediate)]` sets it.
735    ///
736    /// # Panics
737    ///
738    /// When the description already opens at a level or in a mode, by this setter or by
739    /// [`isolation`](Self::isolation). In a `const` the panic is a build error.
740    ///
741    /// # Examples
742    ///
743    /// ```
744    /// # #[cfg(feature = "sqlite")]
745    /// # fn main() -> Result<(), ruststream_sqlx_dialect::StatementError> {
746    /// use ruststream_sqlx_dialect::{Column, Dialect, Form, Mode, Sqlite, TableSpec};
747    ///
748    /// // The handler's transaction takes the write lock before its first read.
749    /// const EMAILS: TableSpec<'static> = TableSpec::new(
750    ///     "email_jobs",
751    ///     Column::new("job_id"),
752    ///     Form::Lease(Column::new("locked_until")),
753    /// )
754    /// .payload(Column::new("payload"))
755    /// .mode(Mode::Immediate);
756    ///
757    /// let begin = Sqlite.begin(EMAILS.opening())?;
758    /// assert_eq!(begin, Some("BEGIN IMMEDIATE"));
759    /// # Ok(())
760    /// # }
761    /// # #[cfg(not(feature = "sqlite"))]
762    /// # fn main() {}
763    /// ```
764    #[must_use]
765    pub const fn mode(self, mode: Mode) -> Self {
766        assert!(
767            matches!(self.opening, Opening::Default),
768            "a table description sets its opening twice (`isolation` or `mode`): a table opens at \
769             one level or in one mode"
770        );
771        Self {
772            opening: Opening::Mode(mode),
773            ..self
774        }
775    }
776}