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}