keepsake_sqlx/repository/
expiry.rs1use chrono::{DateTime, Utc};
2use uuid::Uuid;
3
4use super::{
5 KeepsakeRepository, RelationCache, RepositoryResult, TimedExpiryCandidate, validate_limit,
6};
7
8impl<C> KeepsakeRepository<C>
9where
10 C: RelationCache,
11{
12 pub async fn due_timed_expiry(
14 &self,
15 now: DateTime<Utc>,
16 limit: i64,
17 ) -> RepositoryResult<Vec<TimedExpiryCandidate>> {
18 let limit = validate_limit(limit)?;
19 let rows = sqlx::query_as::<_, TimedExpiryCandidate>(
20 r"
21 select k.id as keepsake_id, k.relation_id, k.subject_kind, k.subject_id, k.expires_at as due_at
22 from keepsakes k
23 join keepsake_relation_definitions r on r.id = k.relation_id
24 where k.state = 'applied'
25 and r.enabled
26 and k.expires_at is not null
27 and k.expires_at <= $1
28 order by k.expires_at, k.relation_id, k.subject_kind, k.subject_id, k.id
29 limit $2
30 ",
31 )
32 .bind(now)
33 .bind(limit)
34 .fetch_all(&self.pool)
35 .await?;
36 Ok(rows)
37 }
38
39 pub async fn expire_due_timed(&self, now: DateTime<Utc>, limit: i64) -> RepositoryResult<u64> {
41 let limit = validate_limit(limit)?;
42 let mut tx = self.pool.begin().await?;
43 let rows = due_timed_expiry_tx(&mut tx, now, limit).await?;
44 let ids = rows
45 .into_iter()
46 .map(|row| row.keepsake_id)
47 .collect::<Vec<Uuid>>();
48 if ids.is_empty() {
49 tx.commit().await?;
50 return Ok(0);
51 }
52
53 let result = sqlx::query(
54 r"
55 update keepsakes
56 set state = 'expired', updated_at = $2
57 where id = any($1)
58 and state = 'applied'
59 and exists (
60 select 1
61 from keepsake_relation_definitions r
62 where r.id = keepsakes.relation_id and r.enabled
63 )
64 ",
65 )
66 .bind(&ids)
67 .bind(now)
68 .execute(&mut *tx)
69 .await?;
70 tx.commit().await?;
71 Ok(result.rows_affected())
72 }
73
74 #[cfg(feature = "fulfillment-counters")]
76 pub async fn upsert_counter_projection(
77 &self,
78 keepsake_id: Uuid,
79 key: &str,
80 value: i64,
81 observed_at: DateTime<Utc>,
82 ) -> RepositoryResult<()> {
83 sqlx::query(
84 r"
85 insert into keepsake_fulfillment_counters
86 (keepsake_id, key, value, observed_at)
87 values ($1, $2, $3, $4)
88 on conflict (keepsake_id, key) do update set
89 value = excluded.value,
90 observed_at = excluded.observed_at
91 ",
92 )
93 .bind(keepsake_id)
94 .bind(key)
95 .bind(value)
96 .bind(observed_at)
97 .execute(&self.pool)
98 .await?;
99 Ok(())
100 }
101}
102
103async fn due_timed_expiry_tx(
104 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
105 now: DateTime<Utc>,
106 limit: i64,
107) -> RepositoryResult<Vec<TimedExpiryCandidate>> {
108 let rows = sqlx::query_as::<_, TimedExpiryCandidate>(
109 r"
110 select k.id as keepsake_id, k.relation_id, k.subject_kind, k.subject_id, k.expires_at as due_at
111 from keepsakes k
112 join keepsake_relation_definitions r on r.id = k.relation_id
113 where k.state = 'applied'
114 and r.enabled
115 and k.expires_at is not null
116 and k.expires_at <= $1
117 order by k.expires_at, k.relation_id, k.subject_kind, k.subject_id, k.id
118 limit $2
119 for update of k skip locked
120 for share of r
121 ",
122 )
123 .bind(now)
124 .bind(limit)
125 .fetch_all(&mut **tx)
126 .await?;
127 Ok(rows)
128}