1use crate::DbBackendConfig;
4use crate::operations::{
5 AppSchema, DatabaseError, DatabaseOperations, FieldType, Mutation, Predicate, Query,
6 Transaction, WriteValue,
7};
8use serde_json::Value;
9use std::collections::BTreeMap;
10
11const MAX_DOCUMENT_BYTES: usize = 1024 * 1024;
12
13pub fn has_due_work(value: &Value) -> bool {
15 let Ok(now) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) else {
16 return false;
17 };
18 let timestamp = |value: Option<&Value>| {
19 value.and_then(Value::as_str).and_then(|text| {
20 text.parse::<u64>().ok().or_else(|| {
21 chrono::DateTime::parse_from_rfc3339(text)
22 .ok()
23 .and_then(|date| u64::try_from(date.timestamp()).ok())
24 })
25 })
26 };
27 [
28 "notification_deliveries",
29 "integration_deliveries",
30 "local_file_deliveries",
31 ]
32 .iter()
33 .any(|section| {
34 value
35 .get(*section)
36 .and_then(Value::as_array)
37 .is_some_and(|rows| {
38 rows.iter().any(|row| {
39 row.get("delivery_status").and_then(Value::as_str) == Some("queued")
40 || (row.get("delivery_status").and_then(Value::as_str) == Some("retryable")
41 && timestamp(row.get("next_attempt_at"))
42 .is_some_and(|due| due <= now.as_secs()))
43 })
44 })
45 }) || value
46 .get("job_executions")
47 .and_then(Value::as_array)
48 .is_some_and(|rows| {
49 rows.iter().any(|row| {
50 row.get("execution_status").and_then(Value::as_str) == Some("queued")
51 && timestamp(row.get("available_at")).is_none_or(|due| due <= now.as_secs())
52 })
53 })
54}
55
56#[derive(Clone)]
58pub struct DocumentSnapshot {
59 pub value: Value,
60 original: Value,
61 entity: String,
62 field: String,
63 identity: Predicate,
64 kind: FieldType,
65}
66
67pub fn read_document(
69 adapter: &dyn DatabaseOperations,
70 config: &DbBackendConfig,
71 schema: &AppSchema,
72 entity: &str,
73 identity: Predicate,
74 field: &str,
75) -> Result<DocumentSnapshot, DatabaseError> {
76 let declaration = schema
77 .entities
78 .iter()
79 .find(|entry| entry.name == entity)
80 .ok_or(DatabaseError::InvalidContract)?;
81 let identity_terms = match &identity {
82 Predicate::Equal(_, _) => vec![&identity],
83 Predicate::All(terms) => terms.iter().collect(),
84 _ => return Err(DatabaseError::InvalidInput),
85 };
86 let mut identity_values = BTreeMap::new();
87 for term in identity_terms {
88 let Predicate::Equal(name, value) = term else {
89 return Err(DatabaseError::InvalidInput);
90 };
91 if value.is_null()
92 || value.is_array()
93 || value.is_object()
94 || identity_values.insert(name.as_str(), value).is_some()
95 {
96 return Err(DatabaseError::InvalidInput);
97 }
98 }
99 let identity_fields = declaration.identity.fields();
100 if identity_values.len() != identity_fields.len()
101 || identity_fields
102 .iter()
103 .any(|name| !identity_values.contains_key(name))
104 {
105 return Err(DatabaseError::InvalidInput);
106 }
107 let kind = declaration
108 .fields
109 .iter()
110 .find(|entry| entry.name == field)
111 .ok_or(DatabaseError::InvalidContract)?
112 .kind;
113 if !matches!(kind, FieldType::Text | FieldType::Json) {
114 return Err(DatabaseError::InvalidContract);
115 }
116 let mut rows = adapter.query(
117 config,
118 schema,
119 &Query {
120 entity: entity.to_owned(),
121 fields: vec![field.to_owned()],
122 filter: Some(identity.clone()),
123 order: Vec::new(),
124 limit: 2,
125 offset: 0,
126 },
127 )?;
128 if rows.len() != 1 {
129 return Err(if rows.is_empty() {
130 DatabaseError::MissingRecord
131 } else {
132 DatabaseError::Conflict
133 });
134 }
135 let original = rows
136 .pop()
137 .and_then(|mut row| row.remove(field))
138 .ok_or(DatabaseError::StorageFailure)?;
139 let value = match kind {
140 FieldType::Text => {
141 let text = original.as_str().ok_or(DatabaseError::StorageFailure)?;
142 if text.len() > MAX_DOCUMENT_BYTES {
143 return Err(DatabaseError::CapacityExceeded);
144 }
145 serde_json::from_str(text).map_err(|_| DatabaseError::StorageFailure)?
146 }
147 FieldType::Json => original.clone(),
148 _ => return Err(DatabaseError::InvalidContract),
149 };
150 if !value.is_object()
151 || serde_json::to_vec(&value)
152 .map_err(|_| DatabaseError::StorageFailure)?
153 .len()
154 > MAX_DOCUMENT_BYTES
155 {
156 return Err(DatabaseError::InvalidInput);
157 }
158 Ok(DocumentSnapshot {
159 value,
160 original,
161 entity: entity.to_owned(),
162 field: field.to_owned(),
163 identity,
164 kind,
165 })
166}
167
168pub fn write_document(
170 adapter: &dyn DatabaseOperations,
171 config: &DbBackendConfig,
172 schema: &AppSchema,
173 snapshot: &DocumentSnapshot,
174 value: &Value,
175) -> Result<(), DatabaseError> {
176 if live_worker_lease(&snapshot.value)? {
177 return Err(DatabaseError::Conflict);
178 }
179 if value.get("_database_worker_lease") != snapshot.value.get("_database_worker_lease") {
180 return Err(DatabaseError::InvalidInput);
181 }
182 compare_and_write(adapter, config, schema, snapshot, value)
183}
184
185pub fn claim_worker(
188 adapter: &dyn DatabaseOperations,
189 config: &DbBackendConfig,
190 schema: &AppSchema,
191 snapshot: &DocumentSnapshot,
192) -> Result<DocumentSnapshot, DatabaseError> {
193 if live_worker_lease(&snapshot.value)? {
194 return Err(DatabaseError::Conflict);
195 }
196 let now = std::time::SystemTime::now()
197 .duration_since(std::time::UNIX_EPOCH)
198 .map_err(|_| DatabaseError::Unavailable)?;
199 static NEXT_TOKEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
200 let token = format!(
201 "{}:{}:{}",
202 std::process::id(),
203 now.as_nanos(),
204 NEXT_TOKEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
205 );
206 let mut value = snapshot.value.clone();
207 value
208 .as_object_mut()
209 .ok_or(DatabaseError::InvalidInput)?
210 .insert(
211 "_database_worker_lease".to_owned(),
212 serde_json::json!({"token": token, "expires_at": now.as_secs().saturating_add(120)}),
213 );
214 compare_and_write(adapter, config, schema, snapshot, &value)?;
215 let original = if snapshot.kind == FieldType::Text {
216 Value::String(serde_json::to_string(&value).map_err(|_| DatabaseError::InvalidInput)?)
217 } else {
218 value.clone()
219 };
220 Ok(DocumentSnapshot {
221 value,
222 original,
223 entity: snapshot.entity.clone(),
224 field: snapshot.field.clone(),
225 identity: snapshot.identity.clone(),
226 kind: snapshot.kind,
227 })
228}
229
230pub fn complete_worker(
232 adapter: &dyn DatabaseOperations,
233 config: &DbBackendConfig,
234 schema: &AppSchema,
235 snapshot: &DocumentSnapshot,
236 value: &Value,
237) -> Result<(), DatabaseError> {
238 if !live_worker_lease(&snapshot.value)?
239 || value.get("_database_worker_lease") != snapshot.value.get("_database_worker_lease")
240 {
241 return Err(DatabaseError::Conflict);
242 }
243 let mut completed = value.clone();
244 completed
245 .as_object_mut()
246 .ok_or(DatabaseError::InvalidInput)?
247 .remove("_database_worker_lease");
248 compare_and_write(adapter, config, schema, snapshot, &completed)
249}
250
251fn live_worker_lease(value: &Value) -> Result<bool, DatabaseError> {
253 let Some(lease) = value.get("_database_worker_lease") else {
254 return Ok(false);
255 };
256 if lease
257 .get("token")
258 .and_then(Value::as_str)
259 .is_none_or(str::is_empty)
260 {
261 return Err(DatabaseError::StorageFailure);
262 }
263 let expiry = lease
264 .get("expires_at")
265 .and_then(Value::as_u64)
266 .ok_or(DatabaseError::StorageFailure)?;
267 let now = std::time::SystemTime::now()
268 .duration_since(std::time::UNIX_EPOCH)
269 .map_err(|_| DatabaseError::Unavailable)?
270 .as_secs();
271 Ok(expiry > now)
272}
273
274fn compare_and_write(
276 adapter: &dyn DatabaseOperations,
277 config: &DbBackendConfig,
278 schema: &AppSchema,
279 snapshot: &DocumentSnapshot,
280 value: &Value,
281) -> Result<(), DatabaseError> {
282 if !value.is_object() {
283 return Err(DatabaseError::InvalidInput);
284 }
285 let encoded = serde_json::to_string(value).map_err(|_| DatabaseError::InvalidInput)?;
286 if encoded.len() > MAX_DOCUMENT_BYTES {
287 return Err(DatabaseError::CapacityExceeded);
288 }
289 if value == &snapshot.value {
290 return Ok(());
291 }
292 let stored = if snapshot.kind == FieldType::Text {
293 Value::String(encoded)
294 } else {
295 value.clone()
296 };
297 adapter.transact(
298 config,
299 schema,
300 &Transaction {
301 steps: vec![Mutation::Update {
302 entity: snapshot.entity.clone(),
303 filter: Predicate::All(vec![
304 snapshot.identity.clone(),
305 Predicate::Equal(snapshot.field.clone(), snapshot.original.clone()),
306 ]),
307 values: BTreeMap::from([(snapshot.field.clone(), WriteValue::Literal(stored))]),
308 expected_rows: Some(1),
309 }],
310 idempotency_key: None,
311 },
312 )?;
313 Ok(())
314}