1use crate::DbBackendConfig;
3use crate::operations::*;
4use serde_json::{Map, Value};
5use std::collections::BTreeMap;
6use std::sync::Arc;
7
8pub struct EffectAdapter {
11 inner: Arc<dyn DatabaseOperations>,
12 effects: Vec<Mutation>,
13 resource: Option<ResourceBinding>,
14}
15
16impl EffectAdapter {
17 pub fn new(
19 inner: Arc<dyn DatabaseOperations>,
20 schema: &AppSchema,
21 effects: Vec<Mutation>,
22 ) -> Result<Self, DatabaseError> {
23 if effects.is_empty()
24 || effects.len() > 64
25 || effects
26 .iter()
27 .any(|step| !matches!(step, Mutation::Insert { .. }))
28 {
29 return Err(DatabaseError::InvalidInput);
30 }
31 validate_transaction(
32 schema,
33 &Transaction {
34 steps: effects.clone(),
35 idempotency_key: None,
36 },
37 )?;
38 Ok(Self {
39 inner,
40 effects,
41 resource: None,
42 })
43 }
44 pub fn with_resource(
46 mut self,
47 schema: &AppSchema,
48 resource: ResourceBinding,
49 ) -> Result<Self, DatabaseError> {
50 validate_resource_binding(schema, &resource)?;
51 self.resource = Some(resource);
52 Ok(self)
53 }
54}
55
56#[derive(Clone)]
58pub struct ResourceBinding {
59 pub entity: String,
60 pub prefix: String,
61}
62
63fn validate_resource_binding(
65 schema: &AppSchema,
66 resource: &ResourceBinding,
67) -> Result<(), DatabaseError> {
68 entity(schema, &resource.entity)?;
69 if resource.prefix.is_empty() || resource.prefix.len() > 128 || !resource.prefix.ends_with(':')
70 {
71 return Err(DatabaseError::InvalidInput);
72 }
73 Ok(())
74}
75
76impl DatabaseOperations for EffectAdapter {
77 fn backend_name(&self) -> &'static str {
79 self.inner.backend_name()
80 }
81 fn local_artifact_format(&self) -> Option<&'static str> {
83 self.inner.local_artifact_format()
84 }
85 fn ensure_database(
87 &self,
88 config: &DbBackendConfig,
89 schema: &AppSchema,
90 ) -> Result<(), DatabaseError> {
91 self.inner.ensure_database(config, schema)
92 }
93 fn user_credentials(
95 &self,
96 config: &DbBackendConfig,
97 schema: &AppSchema,
98 username: &str,
99 ) -> Result<Option<UserCredentials>, DatabaseError> {
100 self.inner.user_credentials(config, schema, username)
101 }
102 fn update_user_password(
104 &self,
105 config: &DbBackendConfig,
106 user_id: i64,
107 password_hash: &str,
108 ) -> Result<(), DatabaseError> {
109 self.inner
110 .update_user_password(config, user_id, password_hash)
111 }
112 fn issue_session(
114 &self,
115 config: &DbBackendConfig,
116 user_id: i64,
117 session_token_hash: &str,
118 expires_at: &str,
119 ) -> Result<(), DatabaseError> {
120 self.inner
121 .issue_session(config, user_id, session_token_hash, expires_at)
122 }
123 fn revoke_session(
125 &self,
126 config: &DbBackendConfig,
127 session_token_hash: &str,
128 ) -> Result<(), DatabaseError> {
129 self.inner.revoke_session(config, session_token_hash)
130 }
131 fn revoke_user_sessions(
133 &self,
134 config: &DbBackendConfig,
135 user_id: i64,
136 ) -> Result<(), DatabaseError> {
137 self.inner.revoke_user_sessions(config, user_id)
138 }
139 fn active_session(
141 &self,
142 config: &DbBackendConfig,
143 schema: &AppSchema,
144 session_token_hash: &str,
145 now_epoch_seconds: u64,
146 ) -> Result<Option<ActiveSession>, DatabaseError> {
147 self.inner
148 .active_session(config, schema, session_token_hash, now_epoch_seconds)
149 }
150 fn append_rate_limit_event(
152 &self,
153 config: &DbBackendConfig,
154 policy_key: &str,
155 bucket_key: &str,
156 outcome: &str,
157 ) -> Result<(), DatabaseError> {
158 self.inner
159 .append_rate_limit_event(config, policy_key, bucket_key, outcome)
160 }
161 fn admit_rate_limited_request(
163 &self,
164 config: &DbBackendConfig,
165 policy_key: &str,
166 bucket_key: &str,
167 maximum: usize,
168 window_seconds: u64,
169 ) -> Result<bool, DatabaseError> {
170 self.inner.admit_rate_limited_request(
171 config,
172 policy_key,
173 bucket_key,
174 maximum,
175 window_seconds,
176 )
177 }
178 fn apply_contract(
180 &self,
181 config: &DbBackendConfig,
182 contract: &crate::contract::AppDataContract,
183 ) -> Result<(), DatabaseError> {
184 self.inner.apply_contract(config, contract)
185 }
186 fn migration_ledger(
188 &self,
189 config: &DbBackendConfig,
190 app_id: &str,
191 ) -> Result<BTreeMap<String, String>, DatabaseError> {
192 self.inner.migration_ledger(config, app_id)
193 }
194 fn applied_migration_checksum(
196 &self,
197 config: &DbBackendConfig,
198 app_id: &str,
199 migration_id: &str,
200 ) -> Result<Option<String>, DatabaseError> {
201 self.inner
202 .applied_migration_checksum(config, app_id, migration_id)
203 }
204 fn verify_contract(
206 &self,
207 config: &DbBackendConfig,
208 contract: &crate::contract::AppDataContract,
209 ) -> Result<(), DatabaseError> {
210 self.inner.verify_contract(config, contract)
211 }
212 fn ready(&self, config: &DbBackendConfig, schema: &AppSchema) -> Result<(), DatabaseError> {
214 self.inner.ready(config, schema)
215 }
216 fn field_exists(
218 &self,
219 config: &DbBackendConfig,
220 schema: &AppSchema,
221 entity_name: &str,
222 field_name: &str,
223 ) -> Result<bool, DatabaseError> {
224 self.inner
225 .field_exists(config, schema, entity_name, field_name)
226 }
227 fn migrate(
229 &self,
230 config: &DbBackendConfig,
231 schema: &AppSchema,
232 migration: &Migration,
233 ) -> Result<(), DatabaseError> {
234 self.inner.migrate(config, schema, migration)
235 }
236 fn adopt_legacy_migration(
238 &self,
239 config: &DbBackendConfig,
240 schema: &AppSchema,
241 migration: &Migration,
242 expected_legacy: &BTreeMap<String, String>,
243 ) -> Result<(), DatabaseError> {
244 self.inner
245 .adopt_legacy_migration(config, schema, migration, expected_legacy)
246 }
247 fn query(
249 &self,
250 config: &DbBackendConfig,
251 schema: &AppSchema,
252 query: &Query,
253 ) -> Result<Vec<Map<String, Value>>, DatabaseError> {
254 self.inner.query(config, schema, query)
255 }
256 fn join_query(
258 &self,
259 config: &DbBackendConfig,
260 schema: &AppSchema,
261 request: &JoinedQuery,
262 ) -> Result<Vec<JoinedRecord>, DatabaseError> {
263 self.inner.join_query(config, schema, request)
264 }
265 fn aggregate(
267 &self,
268 config: &DbBackendConfig,
269 schema: &AppSchema,
270 query: &AggregateQuery,
271 ) -> Result<Value, DatabaseError> {
272 self.inner.aggregate(config, schema, query)
273 }
274 fn transact(
276 &self,
277 config: &DbBackendConfig,
278 schema: &AppSchema,
279 transaction: &Transaction,
280 ) -> Result<TransactionResult, DatabaseError> {
281 transact_with_resource_effects(
282 self.inner.as_ref(),
283 config,
284 schema,
285 transaction,
286 &self.effects,
287 self.resource.as_ref(),
288 )
289 }
290 fn claim_one(
292 &self,
293 config: &DbBackendConfig,
294 schema: &AppSchema,
295 claim: &Claim,
296 ) -> Result<Option<i64>, DatabaseError> {
297 self.inner.claim_one(config, schema, claim)
298 }
299 fn export_page(
301 &self,
302 config: &DbBackendConfig,
303 schema: &AppSchema,
304 entity: &str,
305 after_identity: Option<Value>,
306 limit: u32,
307 ) -> Result<Vec<Record>, DatabaseError> {
308 self.inner
309 .export_page(config, schema, entity, after_identity, limit)
310 }
311 fn export_snapshot(
313 &self,
314 config: &DbBackendConfig,
315 schema: &AppSchema,
316 limits: SnapshotLimits,
317 ) -> Result<Vec<Record>, DatabaseError> {
318 self.inner.export_snapshot(config, schema, limits)
319 }
320 fn restore_snapshot(
322 &self,
323 config: &DbBackendConfig,
324 schema: &AppSchema,
325 records: &[Record],
326 limits: SnapshotLimits,
327 counts: &[crate::snapshot::RestoreCount],
328 ) -> Result<(), DatabaseError> {
329 self.inner
330 .restore_snapshot(config, schema, records, limits, counts)
331 }
332 fn import_batch(
334 &self,
335 config: &DbBackendConfig,
336 schema: &AppSchema,
337 records: &[Record],
338 ) -> Result<(), DatabaseError> {
339 self.inner.import_batch(config, schema, records)
340 }
341}
342
343pub fn transact_with_effects(
346 adapter: &dyn DatabaseOperations,
347 config: &DbBackendConfig,
348 schema: &AppSchema,
349 transaction: &Transaction,
350 effects: &[Mutation],
351) -> Result<TransactionResult, DatabaseError> {
352 transact_with_resource_effects(adapter, config, schema, transaction, effects, None)
353}
354
355#[derive(Clone, Copy, PartialEq, Eq)]
356enum ResourceIdentity {
357 Generated(usize),
358 Existing(i64),
359}
360
361fn resource_identity(
364 schema: &AppSchema,
365 transaction: &Transaction,
366 resource: &ResourceBinding,
367) -> Option<ResourceIdentity> {
368 let identity = entity(schema, &resource.entity)
369 .ok()?
370 .identity
371 .generated_field()?;
372 fn known_id(filter: &Predicate, field: &str) -> Option<i64> {
373 match filter {
374 Predicate::Equal(name, value) if name == field => value.as_i64().filter(|id| *id > 0),
375 Predicate::All(parts) => {
376 let ids = parts
377 .iter()
378 .filter_map(|part| known_id(part, field))
379 .collect::<std::collections::BTreeSet<_>>();
380 if ids.len() == 1 {
381 ids.into_iter().next()
382 } else {
383 None
384 }
385 }
386 _ => None,
387 }
388 }
389 let mut result = None;
390 for (index, step) in transaction.steps.iter().enumerate() {
391 let candidate = match step {
392 Mutation::Insert { entity, .. } | Mutation::InsertIfAbsent { entity, .. }
393 if entity == &resource.entity =>
394 {
395 ResourceIdentity::Generated(index)
396 }
397 Mutation::Update { entity, filter, .. } | Mutation::Delete { entity, filter, .. }
398 if entity == &resource.entity =>
399 {
400 ResourceIdentity::Existing(known_id(filter, identity)?)
401 }
402 Mutation::Upsert { entity, .. } | Mutation::Reserve { entity, .. }
403 if entity == &resource.entity =>
404 {
405 return None;
406 }
407 _ => continue,
408 };
409 if result.is_some_and(|prior| prior != candidate) {
410 return None;
411 }
412 result = Some(candidate);
413 }
414 result
415}
416
417pub fn transact_with_resource_effects(
421 adapter: &dyn DatabaseOperations,
422 config: &DbBackendConfig,
423 schema: &AppSchema,
424 transaction: &Transaction,
425 effects: &[Mutation],
426 resource: Option<&ResourceBinding>,
427) -> Result<TransactionResult, DatabaseError> {
428 if let Some(resource) = resource {
429 validate_resource_binding(schema, resource)?;
430 }
431 if effects.is_empty()
432 || transaction
433 .steps
434 .iter()
435 .all(|step| matches!(step, Mutation::RequireCount { .. }))
436 {
437 return adapter.transact(config, schema, transaction);
438 }
439 if effects.len() > 64
440 || effects
441 .iter()
442 .any(|step| !matches!(step, Mutation::Insert { .. }))
443 {
444 return Err(DatabaseError::InvalidContract);
445 }
446 validate_transaction(
447 schema,
448 &Transaction {
449 steps: effects.to_vec(),
450 idempotency_key: None,
451 },
452 )?;
453 let domain_steps = transaction.steps.len();
454 let binding = resource.and_then(|resource| {
455 resource_identity(schema, transaction, resource).map(|identity| (resource, identity))
456 });
457 let mut composed = transaction.clone();
458 for step in effects {
459 let Mutation::Insert { entity, values } = step else {
460 return Err(DatabaseError::InvalidContract);
461 };
462 let mut values = values.clone();
463 for value in values.values_mut() {
464 value.relocate(domain_steps)?;
465 }
466 if let Some((resource, identity)) = binding {
467 {
468 let index = match identity {
469 ResourceIdentity::Generated(index) => index,
470 ResourceIdentity::Existing(_) => 0,
471 };
472 let marker = format!("{}0", resource.prefix);
473 let declaration = crate::operations::entity(schema, entity)?;
474 for (name, value) in &mut values {
475 let field = declaration
476 .fields
477 .iter()
478 .find(|field| field.name == *name)
479 .ok_or(DatabaseError::InvalidContract)?;
480 if let WriteValue::Literal(literal) = value {
481 if field.kind == FieldType::Text
482 && literal.as_str() == Some(marker.as_str())
483 {
484 *value = WriteValue::GeneratedText {
485 index,
486 prefix: resource.prefix.clone(),
487 };
488 } else if field.kind == FieldType::Json {
489 *value = WriteValue::GeneratedJson {
490 index,
491 marker: marker.clone(),
492 template: literal.clone(),
493 };
494 } else if name == "entity_id"
495 && field.kind == FieldType::Integer
496 && literal.is_null()
497 {
498 *value = WriteValue::GeneratedId(index);
499 }
500 }
501 if let ResourceIdentity::Existing(id) = identity {
502 if value.generated_index() == Some(0)
504 && matches!(
505 value,
506 WriteValue::GeneratedText { .. } | WriteValue::GeneratedJson { .. }
507 )
508 {
509 *value = WriteValue::Literal(resolve_write_value(value, &[Some(id)])?);
510 } else if name == "entity_id" && matches!(value, WriteValue::GeneratedId(0))
511 {
512 *value = WriteValue::Literal(Value::from(id));
513 }
514 }
515 }
516 }
517 }
518 composed.steps.push(Mutation::Insert {
519 entity: entity.clone(),
520 values,
521 });
522 }
523 let mut result = adapter.transact(config, schema, &composed)?;
524 result.affected.truncate(domain_steps);
525 result.generated_ids.truncate(domain_steps);
526 Ok(result)
527}