1use std::any::{Any, TypeId};
2use std::collections::{BTreeMap, HashMap};
3use std::future::Future;
4
5use std::pin::Pin;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Condvar, Mutex, OnceLock};
8use std::time::{Duration, Instant, SystemTime};
9
10use teaql_core::{EntityDescriptor, Value};
11use teaql_sql::{CompiledQuery, DatabaseKind};
12
13use crate::EntityRuntimeState;
14use crate::{
15 CheckObjectStatus, CheckResult, CheckResults, CheckerRegistry, ContextError,
16 EntityDataServiceBehavior, EntityDataServiceBehaviorRegistry, EntityGraphBuilder,
17 EntityRegistry, GraphNode, InMemoryEntityGraphDecoderRegistry, InternalIdGenerator, Language,
18 MetadataStore, ObjectLocation, RawAuditEvent, RawAuditEventSink, RequestPolicy, RuntimeError,
19 local_id_generator,
20};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ContextEntityRef {
24 pub entity_type: String,
25 pub id: u64,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ContextRootError {
30 pub expected_entity_type: String,
31 pub actual_root: Option<ContextEntityRef>,
32}
33
34impl std::fmt::Display for ContextRootError {
35 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match &self.actual_root {
37 None => write!(
38 formatter,
39 "active root {} is missing from UserContext",
40 self.expected_entity_type
41 ),
42 Some(actual) => write!(
43 formatter,
44 "active root type is {}, expected {}",
45 actual.entity_type, self.expected_entity_type
46 ),
47 }
48 }
49}
50
51impl std::error::Error for ContextRootError {}
52
53#[cfg(test)]
54mod active_root_tests {
55 use super::UserContext;
56
57 #[test]
58 fn active_root_is_typed_and_fails_closed() {
59 let context = UserContext::new().with_active_root("Tenant", 42);
60 assert_eq!(context.require_active_root("Tenant").unwrap().id, 42);
61 assert!(context.require_active_root("Organization").is_err());
62 assert!(UserContext::new().require_active_root("Tenant").is_err());
63 }
64}
65
66#[derive(Debug, Clone, PartialEq)]
67pub struct ContinuousPageCursor {
68 pub cursor_id: String,
69 pub query_key: String,
70 pub entity: String,
71 pub direction: teaql_core::SortDirection,
72 pub boundary: Value,
73 pub page_size: u64,
74 pub next_offset: u64,
75 pub expires_at: SystemTime,
76}
77
78#[async_trait::async_trait]
79pub trait ContinuousPageCursorStore: Send + Sync + 'static {
80 async fn get(
81 &self,
82 query_key: &str,
83 target_offset: u64,
84 ) -> Result<Option<ContinuousPageCursor>, String>;
85 async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String>;
86 async fn invalidate(&self, query_key: &str) -> Result<(), String>;
87}
88
89pub struct InMemoryContinuousPageCursorStore {
90 cursors: Mutex<HashMap<String, ContinuousPageCursor>>,
91 max_entries: usize,
92}
93
94#[derive(Debug, Clone)]
95pub struct RetainedIdSet {
96 pub query_key: String,
97 pub ids: Arc<Vec<u64>>,
98 pub expires_at: SystemTime,
99}
100
101#[async_trait::async_trait]
102pub trait IdSetStore: Send + Sync + 'static {
103 async fn get(&self, query_key: &str) -> Result<Option<RetainedIdSet>, String>;
104 async fn put(&self, id_set: RetainedIdSet) -> Result<(), String>;
105 async fn invalidate(&self, query_key: &str) -> Result<(), String>;
106}
107
108pub struct InMemoryIdSetStore {
109 sets: Mutex<HashMap<String, RetainedIdSet>>,
110 max_entries: usize,
111 max_bytes: usize,
112}
113
114impl Default for InMemoryIdSetStore {
115 fn default() -> Self {
116 Self {
117 sets: Mutex::new(HashMap::new()),
118 max_entries: 64,
119 max_bytes: 256 * 1024 * 1024,
120 }
121 }
122}
123
124impl InMemoryIdSetStore {
125 fn retained_bytes(sets: &HashMap<String, RetainedIdSet>) -> usize {
126 sets.values()
127 .map(|value| value.ids.len().saturating_mul(std::mem::size_of::<u64>()))
128 .sum()
129 }
130}
131
132#[async_trait::async_trait]
133impl IdSetStore for InMemoryIdSetStore {
134 async fn get(&self, query_key: &str) -> Result<Option<RetainedIdSet>, String> {
135 let mut sets = self.sets.lock().map_err(|error| error.to_string())?;
136 if sets
137 .get(query_key)
138 .is_some_and(|value| value.expires_at <= SystemTime::now())
139 {
140 sets.remove(query_key);
141 }
142 Ok(sets.get(query_key).cloned())
143 }
144
145 async fn put(&self, id_set: RetainedIdSet) -> Result<(), String> {
146 let incoming_bytes = id_set.ids.len().saturating_mul(std::mem::size_of::<u64>());
147 if incoming_bytes > self.max_bytes {
148 return Err("ID set exceeds the process-local store memory ceiling".to_owned());
149 }
150 let mut sets = self.sets.lock().map_err(|error| error.to_string())?;
151 sets.retain(|_, value| value.expires_at > SystemTime::now());
152 while sets.len() >= self.max_entries
153 || Self::retained_bytes(&sets).saturating_add(incoming_bytes) > self.max_bytes
154 {
155 let Some(oldest) = sets
156 .iter()
157 .min_by_key(|(_, value)| value.expires_at)
158 .map(|(key, _)| key.clone())
159 else {
160 break;
161 };
162 sets.remove(&oldest);
163 }
164 sets.insert(id_set.query_key.clone(), id_set);
165 Ok(())
166 }
167
168 async fn invalidate(&self, query_key: &str) -> Result<(), String> {
169 self.sets
170 .lock()
171 .map_err(|error| error.to_string())?
172 .remove(query_key);
173 Ok(())
174 }
175}
176
177fn id_set_build_lock(query_key: &str) -> Arc<futures_util::lock::Mutex<()>> {
178 static LOCKS: OnceLock<Mutex<HashMap<String, std::sync::Weak<futures_util::lock::Mutex<()>>>>> =
179 OnceLock::new();
180 let mut locks = LOCKS
181 .get_or_init(|| Mutex::new(HashMap::new()))
182 .lock()
183 .expect("ID set build lock registry poisoned");
184 locks.retain(|_, lock| lock.strong_count() > 0);
185 if let Some(lock) = locks.get(query_key).and_then(std::sync::Weak::upgrade) {
186 return lock;
187 }
188 let lock = Arc::new(futures_util::lock::Mutex::new(()));
189 locks.insert(query_key.to_owned(), Arc::downgrade(&lock));
190 lock
191}
192
193impl Default for InMemoryContinuousPageCursorStore {
194 fn default() -> Self {
195 Self {
196 cursors: Mutex::new(HashMap::new()),
197 max_entries: 4096,
198 }
199 }
200}
201
202#[async_trait::async_trait]
203impl ContinuousPageCursorStore for InMemoryContinuousPageCursorStore {
204 async fn get(
205 &self,
206 query_key: &str,
207 target_offset: u64,
208 ) -> Result<Option<ContinuousPageCursor>, String> {
209 let key = format!("{query_key}:{target_offset}");
210 let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
211 if cursors
212 .get(&key)
213 .is_some_and(|cursor| cursor.expires_at <= SystemTime::now())
214 {
215 cursors.remove(&key);
216 }
217 Ok(cursors.get(&key).cloned())
218 }
219
220 async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String> {
221 let key = format!("{}:{}", cursor.query_key, cursor.next_offset);
222 let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
223 if cursors.len() >= self.max_entries {
224 if let Some(expired_or_oldest) = cursors
225 .iter()
226 .min_by_key(|(_, value)| value.expires_at)
227 .map(|(key, _)| key.clone())
228 {
229 cursors.remove(&expired_or_oldest);
230 }
231 }
232 cursors.insert(key, cursor);
233 Ok(())
234 }
235
236 async fn invalidate(&self, query_key: &str) -> Result<(), String> {
237 let prefix = format!("{query_key}:");
238 self.cursors
239 .lock()
240 .map_err(|e| e.to_string())?
241 .retain(|key, _| !key.starts_with(&prefix));
242 Ok(())
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum SqlLogOperation {
248 Select,
249 Insert,
250 Update,
251 Delete,
252 Recover,
253}
254
255impl SqlLogOperation {
256 pub fn is_select(self) -> bool {
257 matches!(self, Self::Select)
258 }
259
260 pub fn is_mutation(self) -> bool {
261 !self.is_select()
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
266pub struct SqlLogOptions {
267 pub select: bool,
268 pub mutation: bool,
269}
270
271impl SqlLogOptions {
272 pub fn disabled() -> Self {
273 Self {
274 select: false,
275 mutation: false,
276 }
277 }
278
279 pub fn select_only() -> Self {
280 Self {
281 select: true,
282 mutation: false,
283 }
284 }
285
286 pub fn mutation_only() -> Self {
287 Self {
288 select: false,
289 mutation: true,
290 }
291 }
292
293 pub fn all() -> Self {
294 Self {
295 select: true,
296 mutation: true,
297 }
298 }
299
300 pub fn enabled_for(self, operation: SqlLogOperation) -> bool {
301 match operation.is_select() {
302 true => self.select,
303 false => self.mutation,
304 }
305 }
306}
307
308#[derive(Debug, Clone, PartialEq)]
309pub struct SqlLogEntry {
310 pub operation: SqlLogOperation,
311 pub sql: String,
312 pub params: Vec<Value>,
313 pub debug_sql: String,
314 pub pretty_sql: String,
315 pub started_at: SystemTime,
316 pub ended_at: SystemTime,
317 pub elapsed: Duration,
318 pub result_count: Option<usize>,
319 pub result_type: Option<String>,
320 pub affected_rows: Option<u64>,
321 pub result_summary: String,
322}
323
324#[derive(Debug, Clone, PartialEq)]
325pub struct UnifiedLogEntry {
326 pub timestamp: SystemTime,
327 pub user_identifier: Option<String>,
328 pub trace_chain: Vec<teaql_core::TraceNode>,
329 pub payload: LogPayload,
330}
331
332#[derive(Debug, Clone, PartialEq)]
333pub enum LogPayload {
334 Sql(SqlLogEntry),
335 Info(InfoLogEntry),
336}
337
338#[derive(Debug, Clone, PartialEq)]
339pub struct InfoLogEntry {
340 pub message: String,
341}
342
343#[derive(Clone, Default)]
344pub struct UnifiedLogBuffer {
345 pub entries: std::sync::Arc<Mutex<Vec<UnifiedLogEntry>>>,
346}
347
348pub struct SchemaInvocation {
355 _context_owned: (),
356}
357
358pub trait SchemaProvider: Send + Sync {
359 fn ensure_schema<'a>(
360 &'a self,
361 context: &'a UserContext,
362 invocation: &'a SchemaInvocation,
363 ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>>;
364}
365
366#[derive(Clone, Debug, PartialEq, Eq)]
367pub enum FixEvidenceSource {
368 Clock,
369 Context,
370}
371
372#[derive(Clone, Debug, PartialEq, Eq)]
373pub struct FixEvidence {
374 pub entity_type: String,
375 pub model_path: String,
376 pub source: FixEvidenceSource,
377 pub source_label: String,
378}
379
380impl FixEvidence {
381 pub fn new(entity_type: &str, model_path: &str, source: FixEvidenceSource, source_label: &str) -> Self {
382 assert!(!entity_type.trim().is_empty(), "entity_type must not be blank");
383 assert!(!model_path.trim().is_empty(), "model_path must not be blank");
384 assert!(!source_label.trim().is_empty(), "source_label must not be blank");
385 let normalized = source_label.to_ascii_lowercase();
386 assert!(!normalized.contains("authorization") && !normalized.contains("cookie") && !normalized.contains("token="),
387 "source_label must be a safe framework label");
388 Self { entity_type: entity_type.to_owned(), model_path: model_path.to_owned(), source, source_label: source_label.to_owned() }
389 }
390}
391
392pub struct UserContext {
393 active_root: Option<ContextEntityRef>,
394 pub(crate) metadata: Option<Box<dyn MetadataStore>>,
395 pub(crate) entity_registry: Option<Box<dyn EntityRegistry>>,
396 pub(crate) entity_graph_decoders: InMemoryEntityGraphDecoderRegistry,
397 pub(crate) entity_data_service_behavior_registry:
398 Option<Box<dyn EntityDataServiceBehaviorRegistry>>,
399 pub(crate) request_policy: Option<Box<dyn RequestPolicy>>,
400 pub(crate) checker_registry: Option<Box<dyn CheckerRegistry>>,
401 pub(crate) event_sink: Option<Box<dyn RawAuditEventSink>>,
402 pub(crate) custom_event_sink: Option<Box<dyn crate::SafeAuditEventSink>>,
403 pub(crate) internal_id_generator: Option<Box<dyn InternalIdGenerator>>,
404 schema_provider: Option<Box<dyn SchemaProvider>>,
405 language: Language,
406 i18n_catalog: Arc<crate::I18nCatalog>,
407 typed_resources: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
408 named_resources: BTreeMap<String, Box<dyn Any + Send + Sync>>,
409 locals: BTreeMap<String, Value>,
410 pub(crate) initial_graphs: Vec<GraphNode>,
411 pub(crate) root_graphs: Vec<GraphNode>,
412 entity_runtime_state: EntityRuntimeState,
413 sql_log_options: SqlLogOptions,
414 sql_log_entries: Mutex<Vec<SqlLogEntry>>,
415 user_identifier: Option<String>,
416 timezone: Option<String>,
417 trace_id: String,
418 continuous_page_cursor_store: std::sync::Arc<dyn ContinuousPageCursorStore>,
419 continuous_page_observation: Mutex<(String, Option<String>)>,
420 id_set_store: Arc<dyn IdSetStore>,
421 id_set_observation: Mutex<(String, Option<u64>)>,
422 local_lock_owner: u64,
423 remote_lock_owner: String,
424 runtime_telemetry: Arc<dyn crate::RuntimeTelemetry>,
425 last_fix_evidence: Mutex<Vec<FixEvidence>>,
426}
427
428#[derive(Clone, Copy)]
429struct LocalLockEntry {
430 owner: u64,
431 expires_at: Option<Instant>,
432}
433
434#[derive(Default)]
435struct ProcessLocalLocks {
436 entries: Mutex<HashMap<String, LocalLockEntry>>,
437 changed: Condvar,
438}
439
440static PROCESS_LOCAL_LOCKS: OnceLock<ProcessLocalLocks> = OnceLock::new();
441static NEXT_LOCAL_LOCK_OWNER: AtomicU64 = AtomicU64::new(1);
442
443impl Default for UserContext {
444 fn default() -> Self {
445 let pid = std::process::id();
446 let thread_id_str = format!("{:?}", std::thread::current().id());
447 let numeric_thread_id = thread_id_str
448 .strip_prefix("ThreadId(")
449 .and_then(|s| s.strip_suffix(")"))
450 .unwrap_or(&thread_id_str);
451 let os_user = std::env::var("USER")
452 .or_else(|_| std::env::var("USERNAME"))
453 .unwrap_or_else(|_| "main".to_owned());
454 let user_id = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
455 let owner_sequence = NEXT_LOCAL_LOCK_OWNER.fetch_add(1, Ordering::Relaxed);
456 Self {
457 active_root: None,
458 metadata: None,
459 entity_registry: None,
460 entity_graph_decoders: InMemoryEntityGraphDecoderRegistry::default(),
461 entity_data_service_behavior_registry: None,
462 request_policy: None,
463 checker_registry: None,
464 event_sink: None,
465 custom_event_sink: None,
466 internal_id_generator: None,
467 schema_provider: None,
468 language: Language::default(),
469 i18n_catalog: crate::I18nCatalog::builtin().clone(),
470 typed_resources: HashMap::new(),
471 named_resources: BTreeMap::new(),
472 locals: BTreeMap::new(),
473 initial_graphs: Vec::new(),
474 root_graphs: Vec::new(),
475 entity_runtime_state: EntityRuntimeState::default(),
476 sql_log_options: SqlLogOptions::all(),
477 sql_log_entries: Mutex::new(Vec::new()),
478 user_identifier: Some(user_id),
479 timezone: Some("UTC".to_owned()),
480 trace_id: format!(
481 "req-{pid}-{numeric_thread_id}-{:x}",
482 std::time::SystemTime::now()
483 .duration_since(std::time::UNIX_EPOCH)
484 .unwrap_or_default()
485 .as_micros()
486 ),
487 continuous_page_cursor_store: std::sync::Arc::new(
488 InMemoryContinuousPageCursorStore::default(),
489 ),
490 continuous_page_observation: Mutex::new(("DISABLED".to_owned(), None)),
491 id_set_store: Arc::new(InMemoryIdSetStore::default()),
492 id_set_observation: Mutex::new(("ID_SET_DISABLED".to_owned(), None)),
493 local_lock_owner: owner_sequence,
494 remote_lock_owner: format!(
495 "teaql:{pid}:{owner_sequence}:{}",
496 SystemTime::now()
497 .duration_since(SystemTime::UNIX_EPOCH)
498 .unwrap_or_default()
499 .as_nanos()
500 ),
501 runtime_telemetry: Arc::new(crate::NoopRuntimeTelemetry),
502 last_fix_evidence: Mutex::new(Vec::new()),
503 }
504 }
505}
506
507#[async_trait::async_trait]
508pub trait DataStore: Send + Sync + 'static {
509 async fn get(&self, key: &str) -> Option<Value>;
510 async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>);
511 async fn remove(&self, key: &str);
512}
513
514#[async_trait::async_trait]
520pub trait RemoteLockProvider: Send + Sync + 'static {
521 async fn try_remote_lock(
522 &self,
523 key: &str,
524 owner_token: &str,
525 timeout_millis: u64,
526 expire_millis: u64,
527 ) -> bool;
528
529 async fn unlock_remote(&self, key: &str, owner_token: &str) -> bool;
530}
531
532#[derive(Default)]
533pub struct InMemoryDataStore {
534 cache: std::sync::RwLock<HashMap<String, (Value, Option<std::time::Instant>)>>,
535}
536
537#[async_trait::async_trait]
538impl DataStore for InMemoryDataStore {
539 async fn get(&self, key: &str) -> Option<Value> {
540 let lock = self.cache.read().unwrap();
541 if let Some((val, expires_at)) = lock.get(key) {
542 if let Some(exp) = expires_at {
543 if std::time::Instant::now() > *exp {
544 return None;
545 }
546 }
547 return Some(val.clone());
548 }
549 None
550 }
551
552 async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>) {
553 let mut lock = self.cache.write().unwrap();
554 let expires_at = timeout_seconds
555 .map(|secs| std::time::Instant::now() + std::time::Duration::from_secs(secs));
556 lock.insert(key.to_string(), (value, expires_at));
557 }
558
559 async fn remove(&self, key: &str) {
560 let mut lock = self.cache.write().unwrap();
561 lock.remove(key);
562 }
563}
564
565impl UserContext {
566 pub fn new() -> Self {
567 Self::default()
568 }
569
570 pub fn with_active_root(mut self, entity_type: impl Into<String>, id: u64) -> Self {
571 let entity_type = entity_type.into();
572 assert!(
573 !entity_type.trim().is_empty(),
574 "active root entity type is required"
575 );
576 assert!(id > 0, "active root id must be positive");
577 self.active_root = Some(ContextEntityRef { entity_type, id });
578 self
579 }
580
581 pub fn require_active_root(
582 &self,
583 expected_entity_type: &str,
584 ) -> Result<&ContextEntityRef, ContextRootError> {
585 match &self.active_root {
586 Some(root) if root.entity_type == expected_entity_type => Ok(root),
587 actual_root => Err(ContextRootError {
588 expected_entity_type: expected_entity_type.to_owned(),
589 actual_root: actual_root.clone(),
590 }),
591 }
592 }
593
594 pub(crate) fn active_root_ref(&self) -> Option<&ContextEntityRef> {
595 self.active_root.as_ref()
596 }
597
598 pub fn with_runtime_telemetry(mut self, telemetry: Arc<dyn crate::RuntimeTelemetry>) -> Self {
599 self.runtime_telemetry = telemetry;
600 self
601 }
602
603 pub fn set_runtime_telemetry(&mut self, telemetry: Arc<dyn crate::RuntimeTelemetry>) {
604 self.runtime_telemetry = telemetry;
605 }
606
607 pub fn runtime_telemetry(&self) -> &Arc<dyn crate::RuntimeTelemetry> {
608 &self.runtime_telemetry
609 }
610
611 pub(crate) fn runtime_telemetry_is_noop(&self) -> bool {
612 self.runtime_telemetry.is_noop()
613 }
614
615 pub fn start_runtime_operation(
616 &self,
617 operation: crate::RuntimeOperation,
618 ) -> crate::FailOpenRuntimeTelemetryScope {
619 crate::start_runtime_operation(&self.runtime_telemetry, operation)
620 }
621
622 pub fn try_local_lock(&self, key: &str, timeout_millis: u64, expire_millis: u64) -> bool {
623 let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
624 let deadline = Instant::now() + Duration::from_millis(timeout_millis);
625 let mut entries = locks.entries.lock().expect("local lock state poisoned");
626 loop {
627 let now = Instant::now();
628 match entries.get(key).copied() {
629 None => {
630 entries.insert(
631 key.to_owned(),
632 LocalLockEntry {
633 owner: self.local_lock_owner,
634 expires_at: (expire_millis > 0)
635 .then(|| now + Duration::from_millis(expire_millis)),
636 },
637 );
638 return true;
639 }
640 Some(current)
641 if current.owner == self.local_lock_owner
642 || current.expires_at.is_some_and(|expiry| now >= expiry) =>
643 {
644 entries.insert(
645 key.to_owned(),
646 LocalLockEntry {
647 owner: self.local_lock_owner,
648 expires_at: (expire_millis > 0)
649 .then(|| now + Duration::from_millis(expire_millis)),
650 },
651 );
652 return true;
653 }
654 Some(current) => {
655 if timeout_millis == 0 || now >= deadline {
656 return false;
657 }
658 let wake_after = current
659 .expires_at
660 .map(|expiry| expiry.saturating_duration_since(now))
661 .unwrap_or_else(|| deadline.saturating_duration_since(now))
662 .min(deadline.saturating_duration_since(now));
663 let waited = locks
664 .changed
665 .wait_timeout(entries, wake_after)
666 .expect("local lock state poisoned");
667 entries = waited.0;
668 }
669 }
670 }
671 }
672
673 pub fn unlock_local(&self, key: &str) {
674 let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
675 let mut entries = locks.entries.lock().expect("local lock state poisoned");
676 if entries
677 .get(key)
678 .is_some_and(|entry| entry.owner == self.local_lock_owner)
679 {
680 entries.remove(key);
681 locks.changed.notify_all();
682 }
683 }
684
685 pub async fn try_remote_lock(
691 &self,
692 key: &str,
693 timeout_millis: u64,
694 expire_millis: u64,
695 ) -> bool {
696 match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
697 Some(provider) => {
698 provider
699 .try_remote_lock(key, &self.remote_lock_owner, timeout_millis, expire_millis)
700 .await
701 }
702 None => true,
703 }
704 }
705
706 pub async fn unlock_remote(&self, key: &str) -> bool {
708 match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
709 Some(provider) => provider.unlock_remote(key, &self.remote_lock_owner).await,
710 None => true,
711 }
712 }
713
714 pub fn user_identifier(&self) -> Option<&str> {
715 self.user_identifier.as_deref()
716 }
717
718 pub fn set_user_identifier(&mut self, user_identifier: impl Into<String>) {
719 self.user_identifier = Some(user_identifier.into());
720 }
721
722 pub fn set_continuous_page_cursor_store(
723 &mut self,
724 store: std::sync::Arc<dyn ContinuousPageCursorStore>,
725 ) {
726 self.continuous_page_cursor_store = store;
727 }
728
729 pub fn continuous_page_plan(&self) -> Option<String> {
730 self.continuous_page_observation
731 .lock()
732 .ok()
733 .map(|value| value.0.clone())
734 }
735
736 pub fn continuous_page_cursor_id(&self) -> Option<String> {
737 self.continuous_page_observation
738 .lock()
739 .ok()
740 .and_then(|value| value.1.clone())
741 }
742
743 pub(crate) fn observe_continuous_page(
744 &self,
745 plan: impl Into<String>,
746 cursor_id: Option<String>,
747 ) {
748 if let Ok(mut observation) = self.continuous_page_observation.lock() {
749 *observation = (plan.into(), cursor_id);
750 }
751 }
752
753 pub(crate) fn continuous_page_cursor_store(&self) -> &dyn ContinuousPageCursorStore {
754 self.continuous_page_cursor_store.as_ref()
755 }
756
757 pub fn set_id_set_store(&mut self, store: Arc<dyn IdSetStore>) {
758 self.id_set_store = store;
759 }
760
761 pub fn id_set_plan(&self) -> Option<String> {
762 self.id_set_observation
763 .lock()
764 .ok()
765 .map(|observation| observation.0.clone())
766 }
767
768 pub fn id_set_count(&self) -> Option<u64> {
769 self.id_set_observation
770 .lock()
771 .ok()
772 .and_then(|observation| observation.1)
773 }
774
775 pub(crate) fn observe_id_set(&self, plan: impl Into<String>, count: Option<u64>) {
776 if let Ok(mut observation) = self.id_set_observation.lock() {
777 *observation = (plan.into(), count);
778 }
779 }
780
781 pub(crate) fn id_set_store(&self) -> &dyn IdSetStore {
782 self.id_set_store.as_ref()
783 }
784
785 pub(crate) fn id_set_build_lock(&self, query_key: &str) -> Arc<futures_util::lock::Mutex<()>> {
786 id_set_build_lock(query_key)
787 }
788
789 pub fn with_user_identifier(mut self, user_identifier: impl Into<String>) -> Self {
790 self.user_identifier = Some(user_identifier.into());
791 self
792 }
793
794 pub fn set_user_identifier_option(&mut self, user_identifier: Option<String>) {
795 self.user_identifier = user_identifier;
796 }
797
798 pub fn with_user_identifier_option(mut self, user_identifier: Option<String>) -> Self {
799 self.user_identifier = user_identifier;
800 self
801 }
802
803 pub fn timezone(&self) -> Option<&str> {
804 self.timezone.as_deref()
805 }
806
807 pub fn set_timezone(&mut self, timezone: impl Into<String>) {
808 self.timezone = Some(timezone.into());
809 }
810
811 pub fn with_timezone(mut self, timezone: impl Into<String>) -> Self {
812 self.timezone = Some(timezone.into());
813 self
814 }
815
816 pub fn trace_id(&self) -> &str {
817 &self.trace_id
818 }
819
820 pub fn set_trace_id(&mut self, trace_id: impl Into<String>) {
821 self.trace_id = trace_id.into();
822 }
823
824 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
825 self.trace_id = trace_id.into();
826 self
827 }
828
829 pub fn with_module(mut self, module: crate::RuntimeModule) -> Self {
830 module.apply_to(&mut self);
831 self
832 }
833
834 pub fn entity_runtime_state(&self) -> EntityRuntimeState {
835 EntityRuntimeState::fresh_with_shared_graph(&self.entity_runtime_state)
838 }
839
840 pub fn initial_graphs(&self) -> &[GraphNode] {
841 &self.initial_graphs
842 }
843
844 pub fn set_initial_graphs(&mut self, graphs: Vec<GraphNode>) {
845 self.initial_graphs = graphs;
846 }
847
848 pub fn root_graphs(&self) -> &[GraphNode] {
849 &self.root_graphs
850 }
851
852 pub fn set_root_graphs(&mut self, graphs: Vec<GraphNode>) {
853 self.root_graphs = graphs;
854 }
855
856 pub fn with_metadata(mut self, metadata: impl MetadataStore + 'static) -> Self {
857 self.metadata = Some(Box::new(metadata));
858 self
859 }
860
861 pub fn set_metadata(&mut self, metadata: impl MetadataStore + 'static) {
862 self.metadata = Some(Box::new(metadata));
863 }
864
865 pub fn with_entity_registry(mut self, registry: impl EntityRegistry + 'static) -> Self {
866 self.entity_registry = Some(Box::new(registry));
867 self
868 }
869
870 pub fn set_entity_registry(&mut self, registry: impl EntityRegistry + 'static) {
871 self.entity_registry = Some(Box::new(registry));
872 }
873
874 pub fn set_entity_graph_decoder_registry(
875 &mut self,
876 registry: InMemoryEntityGraphDecoderRegistry,
877 ) {
878 self.entity_graph_decoders = registry;
879 }
880
881 pub(crate) fn has_entity_graph_decoder(&self, entity: &str) -> bool {
882 self.entity_graph_decoders.contains(entity)
883 }
884
885 pub(crate) fn decode_compact_entity_into_graph(
886 &self,
887 entity: &str,
888 row: teaql_core::CompactRow,
889 root: &EntityRuntimeState,
890 graph: &mut EntityGraphBuilder,
891 ) -> Result<(), teaql_core::EntityError> {
892 self.entity_graph_decoders
893 .decode_compact(entity, row, root, graph)
894 }
895
896 pub(crate) fn decode_compact_entity_list_into_graph(
897 &self,
898 entity: &str,
899 rows: Vec<teaql_core::CompactRow>,
900 root: &EntityRuntimeState,
901 graph: &mut EntityGraphBuilder,
902 owner_entity: &str,
903 owner_id: u64,
904 relation: &str,
905 ) -> Result<(), teaql_core::EntityError> {
906 self.entity_graph_decoders.decode_compact_list(
907 entity,
908 rows,
909 root,
910 graph,
911 owner_entity,
912 owner_id,
913 relation,
914 )
915 }
916
917 pub(crate) fn decode_compact_entity_batch_into_graph(
918 &self,
919 entity: &str,
920 rows: Vec<teaql_core::CompactRow>,
921 root: &EntityRuntimeState,
922 graph: &mut EntityGraphBuilder,
923 ) -> Result<(), teaql_core::EntityError> {
924 self.entity_graph_decoders
925 .decode_compact_batch(entity, rows, root, graph)
926 }
927
928 pub(crate) fn decode_compact_entity_option_into_graph(
929 &self,
930 entity: &str,
931 rows: Vec<teaql_core::CompactRow>,
932 root: &EntityRuntimeState,
933 graph: &mut EntityGraphBuilder,
934 owner_entity: &str,
935 owner_id: u64,
936 relation: &str,
937 ) -> Result<(), teaql_core::EntityError> {
938 self.entity_graph_decoders.decode_compact_option(
939 entity,
940 rows,
941 root,
942 graph,
943 owner_entity,
944 owner_id,
945 relation,
946 )
947 }
948
949 pub fn with_entity_data_service_behavior_registry(
950 mut self,
951 registry: impl EntityDataServiceBehaviorRegistry + 'static,
952 ) -> Self {
953 self.entity_data_service_behavior_registry = Some(Box::new(registry));
954 self
955 }
956
957 pub fn set_entity_data_service_behavior_registry(
958 &mut self,
959 registry: impl EntityDataServiceBehaviorRegistry + 'static,
960 ) {
961 self.entity_data_service_behavior_registry = Some(Box::new(registry));
962 }
963
964 pub fn with_request_policy(mut self, policy: impl RequestPolicy + 'static) -> Self {
965 self.request_policy = Some(Box::new(policy));
966 self
967 }
968
969 pub fn set_request_policy(&mut self, policy: impl RequestPolicy + 'static) {
970 self.request_policy = Some(Box::new(policy));
971 }
972
973 pub fn clear_request_policy(&mut self) {
974 self.request_policy = None;
975 }
976
977 pub fn with_checker_registry(mut self, registry: impl CheckerRegistry + 'static) -> Self {
978 self.checker_registry = Some(Box::new(registry));
979 self
980 }
981
982 pub fn set_checker_registry(&mut self, registry: impl CheckerRegistry + 'static) {
983 self.checker_registry = Some(Box::new(registry));
984 }
985
986 pub(crate) fn with_event_sink(mut self, sink: impl RawAuditEventSink + 'static) -> Self {
987 self.event_sink = Some(Box::new(sink));
988 self
989 }
990
991 pub(crate) fn set_event_sink(&mut self, sink: impl RawAuditEventSink + 'static) {
992 self.event_sink = Some(Box::new(sink));
993 }
994
995 pub fn with_custom_event_sink(
996 mut self,
997 sink: impl crate::SafeAuditEventSink + 'static,
998 ) -> Self {
999 self.custom_event_sink = Some(Box::new(sink));
1000 self
1001 }
1002
1003 pub fn set_custom_event_sink(&mut self, sink: impl crate::SafeAuditEventSink + 'static) {
1004 self.custom_event_sink = Some(Box::new(sink));
1005 }
1006
1007 pub fn with_internal_id_generator(
1008 mut self,
1009 generator: impl InternalIdGenerator + 'static,
1010 ) -> Self {
1011 self.internal_id_generator = Some(Box::new(generator));
1012 self
1013 }
1014
1015 pub fn set_internal_id_generator(&mut self, generator: impl InternalIdGenerator + 'static) {
1016 self.internal_id_generator = Some(Box::new(generator));
1017 }
1018
1019 pub fn with_schema_provider(mut self, provider: impl SchemaProvider + 'static) -> Self {
1020 self.schema_provider = Some(Box::new(provider));
1021 self
1022 }
1023
1024 pub fn set_schema_provider(&mut self, provider: impl SchemaProvider + 'static) {
1025 self.schema_provider = Some(Box::new(provider));
1026 }
1027
1028 pub async fn ensure_schema(&self) -> Result<(), RuntimeError> {
1029 let provider = self
1030 .schema_provider
1031 .as_ref()
1032 .ok_or_else(|| RuntimeError::Schema("missing schema provider".to_owned()))?;
1033 let invocation = SchemaInvocation { _context_owned: () };
1034 provider.ensure_schema(self, &invocation).await
1035 }
1036
1037 pub fn with_language(mut self, language: Language) -> Self {
1038 self.language = language;
1039 self
1040 }
1041
1042 pub fn set_language(&mut self, language: Language) {
1043 self.language = language;
1044 }
1045
1046 pub fn with_i18n_catalog(mut self, catalog: Arc<crate::I18nCatalog>) -> Self {
1047 self.i18n_catalog = catalog;
1048 self
1049 }
1050
1051 pub fn set_i18n_catalog(&mut self, catalog: Arc<crate::I18nCatalog>) {
1052 self.i18n_catalog = catalog;
1053 }
1054
1055 pub fn with_sql_log_options(mut self, options: SqlLogOptions) -> Self {
1056 self.sql_log_options = options;
1057 self
1058 }
1059
1060 pub fn set_sql_log_options(&mut self, options: SqlLogOptions) {
1061 self.sql_log_options = options;
1062 }
1063
1064 pub fn enable_select_sql_log(&mut self) {
1065 self.sql_log_options.select = true;
1066 }
1067
1068 pub fn enable_mutation_sql_log(&mut self) {
1069 self.sql_log_options.mutation = true;
1070 }
1071
1072 pub fn enable_all_sql_log(&mut self) {
1073 self.sql_log_options = SqlLogOptions::all();
1074 }
1075
1076 pub fn disable_sql_log(&mut self) {
1077 self.sql_log_options = SqlLogOptions::disabled();
1078 self.clear_sql_logs();
1079 }
1080
1081 pub fn sql_log_options(&self) -> SqlLogOptions {
1082 self.sql_log_options
1083 }
1084
1085 pub fn sql_logs(&self) -> Vec<SqlLogEntry> {
1086 self.sql_log_entries
1087 .lock()
1088 .map(|entries| entries.clone())
1089 .unwrap_or_default()
1090 }
1091
1092 pub fn clear_sql_logs(&self) {
1093 if let Ok(mut entries) = self.sql_log_entries.lock() {
1094 entries.clear();
1095 }
1096 }
1097
1098 pub(crate) fn record_sql_log(
1099 &self,
1100 operation: SqlLogOperation,
1101 query: &CompiledQuery,
1102 database_kind: DatabaseKind,
1103 started_at: SystemTime,
1104 ended_at: SystemTime,
1105 elapsed: Duration,
1106 result_count: Option<usize>,
1107 result_type: Option<String>,
1108 affected_rows: Option<u64>,
1109 trace_chain: Vec<teaql_core::TraceNode>,
1110 ) {
1111 if !self.sql_log_options.enabled_for(operation) {
1112 return;
1113 }
1114 let debug_sql = query.debug_sql(database_kind);
1115 let result_summary = sql_result_summary(
1116 operation,
1117 result_count,
1118 result_type.as_deref(),
1119 affected_rows,
1120 &debug_sql,
1121 );
1122
1123 let sql_log_entry = SqlLogEntry {
1124 operation,
1125 sql: query.sql.clone(),
1126 params: query.params.clone(),
1127 pretty_sql: pretty_sql(&debug_sql),
1128 debug_sql: debug_sql.clone(),
1129 started_at,
1130 ended_at,
1131 elapsed,
1132 result_summary: result_summary.clone(),
1133 result_count,
1134 result_type,
1135 affected_rows,
1136 };
1137
1138 if let Ok(mut entries) = self.sql_log_entries.lock() {
1139 entries.push(sql_log_entry.clone());
1143 }
1144
1145 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
1146 if let Ok(mut entries) = buf.entries.lock() {
1147 entries.push(UnifiedLogEntry {
1148 timestamp: started_at,
1149 user_identifier: self.user_identifier.clone(),
1150 trace_chain: trace_chain.clone(),
1151 payload: LogPayload::Sql(sql_log_entry.clone()),
1152 });
1153 }
1154 }
1155
1156 crate::log_formatter::LogManager::write_sql_log(&trace_chain, &sql_log_entry);
1157 }
1158
1159 pub(crate) fn record_metadata_log(&self, metadata: &teaql_data_service::ExecutionMetadata) {
1160 let operation = match metadata.operation {
1161 teaql_data_service::DataServiceOperation::Query => SqlLogOperation::Select,
1162 teaql_data_service::DataServiceOperation::Insert => SqlLogOperation::Insert,
1163 teaql_data_service::DataServiceOperation::Update => SqlLogOperation::Update,
1164 teaql_data_service::DataServiceOperation::Delete => SqlLogOperation::Delete,
1165 teaql_data_service::DataServiceOperation::Recover => SqlLogOperation::Update,
1166 teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
1167 teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
1168 };
1169 if !self.sql_log_options.enabled_for(operation) {
1170 return;
1171 }
1172 if let Some(debug_sql) = &metadata.debug_query {
1173 let sql_log_entry = SqlLogEntry {
1174 operation,
1175 sql: metadata.parameterized_query.clone().unwrap_or_default(),
1176 params: metadata.params.clone(),
1177 pretty_sql: pretty_sql(debug_sql),
1178 debug_sql: debug_sql.clone(),
1179 started_at: metadata.started_at,
1180 ended_at: metadata.ended_at,
1181 elapsed: metadata
1182 .ended_at
1183 .duration_since(metadata.started_at)
1184 .unwrap_or_default(),
1185 result_count: metadata.result_count,
1186 result_type: None, affected_rows: metadata.affected_rows,
1188 result_summary: String::new(), };
1190
1191 let mut summary = String::new();
1193 if let Some(c) = metadata.result_count {
1194 summary = format!("{} rows returned", c);
1195 } else if let Some(a) = metadata.affected_rows {
1196 summary = format!("{} rows affected", a);
1197 }
1198
1199 let mut final_entry = sql_log_entry;
1200 final_entry.result_summary = summary;
1201
1202 if let Ok(mut entries) = self.sql_log_entries.lock() {
1203 entries.push(final_entry.clone());
1204 }
1205
1206 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
1207 if let Ok(mut entries) = buf.entries.lock() {
1208 entries.push(UnifiedLogEntry {
1209 timestamp: metadata.started_at,
1210 user_identifier: self.user_identifier.clone(),
1211 trace_chain: metadata.trace_chain.clone(),
1212 payload: LogPayload::Sql(final_entry.clone()),
1213 });
1214 }
1215 }
1216
1217 crate::log_formatter::LogManager::write_sql_log(&metadata.trace_chain, &final_entry);
1218 }
1219 }
1220
1221 pub fn language(&self) -> Language {
1222 self.language
1223 }
1224
1225 pub fn set_language_code(&mut self, code: &str) -> Result<(), RuntimeError> {
1226 let Some(language) = Language::from_code(code) else {
1227 return Err(RuntimeError::UnsupportedLocale(code.to_owned()));
1228 };
1229 self.language = language;
1230 Ok(())
1231 }
1232
1233 pub fn set_locale_code(&mut self, code: &str) -> Result<(), RuntimeError> {
1234 self.set_language_code(code)
1235 }
1236
1237 pub fn generate_id(&self, entity: &str) -> Result<Option<u64>, RuntimeError> {
1238 self.internal_id_generator
1239 .as_ref()
1240 .map(|generator| generator.generate_id(entity))
1241 .transpose()
1242 }
1243
1244 pub fn next_id(&self, entity: &str) -> Result<u64, RuntimeError> {
1245 match self.generate_id(entity)? {
1246 Some(id) => Ok(id),
1247 None => local_id_generator().generate_id(entity),
1248 }
1249 }
1250
1251 pub fn entity(&self, name: &str) -> Option<&EntityDescriptor> {
1252 self.metadata
1253 .as_ref()
1254 .and_then(|metadata| metadata.entity(name))
1255 }
1256
1257 pub fn all_entities(&self) -> Vec<&EntityDescriptor> {
1258 self.metadata
1259 .as_ref()
1260 .map(|metadata| metadata.all_entities())
1261 .unwrap_or_default()
1262 }
1263
1264 pub fn require_entity(&self, name: &str) -> Result<&EntityDescriptor, RuntimeError> {
1265 self.entity(name)
1266 .ok_or_else(|| RuntimeError::MissingEntity(name.to_owned()))
1267 }
1268
1269 pub fn insert_resource<T>(&mut self, resource: T)
1270 where
1271 T: Send + Sync + 'static,
1272 {
1273 self.typed_resources
1274 .insert(TypeId::of::<T>(), Box::new(resource));
1275 }
1276
1277 pub fn get_resource<T>(&self) -> Option<&T>
1278 where
1279 T: Send + Sync + 'static,
1280 {
1281 self.typed_resources
1282 .get(&TypeId::of::<T>())
1283 .and_then(|value| value.downcast_ref::<T>())
1284 }
1285
1286 pub fn require_resource<T>(&self) -> Result<&T, ContextError>
1287 where
1288 T: Send + Sync + 'static,
1289 {
1290 self.get_resource::<T>()
1291 .ok_or(ContextError::MissingTypedResource(
1292 std::any::type_name::<T>(),
1293 ))
1294 }
1295
1296 pub fn insert_named_resource<T>(&mut self, name: impl Into<String>, resource: T)
1297 where
1298 T: Send + Sync + 'static,
1299 {
1300 self.named_resources.insert(name.into(), Box::new(resource));
1301 }
1302
1303 pub fn get_named_resource<T>(&self, name: &str) -> Option<&T>
1304 where
1305 T: Send + Sync + 'static,
1306 {
1307 self.named_resources
1308 .get(name)
1309 .and_then(|value| value.downcast_ref::<T>())
1310 }
1311
1312 pub fn require_named_resource<T>(&self, name: &str) -> Result<&T, ContextError>
1313 where
1314 T: Send + Sync + 'static,
1315 {
1316 self.get_named_resource::<T>(name)
1317 .ok_or_else(|| ContextError::MissingResource(name.to_owned()))
1318 }
1319
1320 pub fn put_local(&mut self, key: impl Into<String>, value: impl Into<Value>) {
1321 self.locals.insert(key.into(), value.into());
1322 }
1323
1324 pub fn local(&self, key: &str) -> Option<&Value> {
1325 self.locals.get(key)
1326 }
1327
1328 pub fn remove_local(&mut self, key: &str) -> Option<Value> {
1329 self.locals.remove(key)
1330 }
1331
1332 pub fn has_entity_data_service(&self, entity: &str) -> bool {
1333 let in_registry = self
1334 .entity_registry
1335 .as_ref()
1336 .map(|registry| registry.contains(entity))
1337 .unwrap_or(false);
1338 in_registry || self.entity(entity).is_some()
1339 }
1340
1341 pub fn entity_data_service_behavior(
1342 &self,
1343 entity: &str,
1344 ) -> Option<std::sync::Arc<dyn EntityDataServiceBehavior>> {
1345 self.entity_data_service_behavior_registry
1346 .as_ref()
1347 .and_then(|registry| registry.behavior(entity))
1348 }
1349
1350 pub fn has_checker(&self, entity: &str) -> bool {
1351 self.checker_registry
1352 .as_ref()
1353 .and_then(|registry| registry.checker(entity))
1354 .is_some()
1355 }
1356
1357 pub fn fix_time(&self) -> teaql_core::time::Timestamp {
1361 crate::entity_save::current_graph_fix_time()
1362 }
1363
1364 pub fn record_fix_evidence(&self, evidence: FixEvidence) {
1365 crate::entity_save::record_graph_fix_evidence(evidence);
1366 }
1367
1368 pub(crate) fn replace_last_fix_evidence(&self, evidence: Vec<FixEvidence>) {
1369 *self.last_fix_evidence.lock().unwrap() = evidence;
1370 }
1371
1372 pub fn last_fix_evidence(&self) -> Vec<FixEvidence> {
1373 self.last_fix_evidence.lock().unwrap().clone()
1374 }
1375
1376 pub fn check_and_fix_values(
1377 &self,
1378 entity: &str,
1379 values: &mut crate::EntityValues,
1380 ) -> Result<(), RuntimeError> {
1381 self.check_and_fix_values_at(entity, values, &ObjectLocation::root())
1382 }
1383
1384 pub fn check_and_fix_values_at(
1385 &self,
1386 entity: &str,
1387 values: &mut crate::EntityValues,
1388 location: &ObjectLocation,
1389 ) -> Result<(), RuntimeError> {
1390 let status = CheckObjectStatus::from_values(values);
1391 let checker = self
1392 .checker_registry
1393 .as_ref()
1394 .and_then(|registry| registry.checker(entity));
1395 let mut results = CheckResults::new();
1396 if let Some(checker) = checker {
1397 checker.check_and_fix(self, values, location, &mut results);
1398 }
1399
1400 if let Some(descriptor) = self
1405 .metadata
1406 .as_ref()
1407 .and_then(|metadata| metadata.entity(entity))
1408 {
1409 for property in descriptor
1410 .properties
1411 .iter()
1412 .filter(|property| !property.nullable && !property.is_version)
1416 {
1417 let missing = !values.contains_key(&property.name);
1418 let null = matches!(values.get(&property.name), Some(Value::Null));
1419 let property_location = location.clone().member(&property.name);
1420 let already_reported = results.iter().any(|result| {
1421 result.rule == crate::CheckRule::Required
1422 && result.location == property_location
1423 });
1424 if ((status.is_create() && missing) || null) && !already_reported {
1425 results.push(CheckResult::required(property_location));
1426 }
1427 }
1428 }
1429 if results.is_empty() {
1430 return Ok(());
1431 }
1432 self.translate_check_results(&mut results);
1433 Err(RuntimeError::Check(results))
1434 }
1435
1436 pub fn translate_check_results(&self, results: &mut CheckResults) {
1437 for result in results {
1438 if result.message.is_none() {
1439 result.message = Some(
1440 self.i18n_catalog
1441 .translate_check_result(self.language, result),
1442 );
1443 }
1444 }
1445 }
1446
1447 pub fn send_event(&self, event: RawAuditEvent) -> Result<(), RuntimeError> {
1448 let scope = self.start_runtime_operation(
1449 crate::RuntimeOperation::new("audit", format!("{}.event", event.entity))
1450 .attribute("teaql.entity.type", event.entity.clone()),
1451 );
1452 let result = self.send_event_inner(event);
1453 match &result {
1454 Ok(()) => scope.success(std::collections::BTreeMap::new()),
1455 Err(_) => scope.failure("audit_error"),
1456 }
1457 result
1458 }
1459
1460 fn send_event_inner(&self, event: RawAuditEvent) -> Result<(), RuntimeError> {
1461 if let Some(sink) = self.event_sink.as_ref() {
1462 sink.on_event(self, &event)?;
1463 }
1464 if let Some(sink) = self.custom_event_sink.as_ref() {
1465 let (mask_fields, max_len) = self
1466 .metadata
1467 .as_ref()
1468 .and_then(|metadata| metadata.entity(&event.entity))
1469 .map(|desc| (desc.audit_mask_fields.clone(), desc.audit_value_max_len))
1470 .unwrap_or_else(|| (vec![], None));
1471
1472 let safe_event = event.build_safe_event(&mask_fields, max_len);
1473 sink.on_safe_event(self, &safe_event)?;
1474 }
1475
1476 crate::log_formatter::LogManager::write_audit_log(&event);
1477
1478 Ok(())
1479 }
1480
1481 pub async fn get_in_store(&self, key: &str) -> Option<Value> {
1482 let store = self.get_resource::<Box<dyn DataStore>>()?;
1483 store.get(key).await
1484 }
1485
1486 pub async fn put_in_store(
1487 &self,
1488 key: &str,
1489 value: impl Into<Value>,
1490 timeout_seconds: Option<u64>,
1491 ) {
1492 if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1493 store.put(key, value.into(), timeout_seconds).await;
1494 }
1495 }
1496
1497 pub async fn clear_in_store(&self, key: &str) {
1498 if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1499 store.remove(key).await;
1500 }
1501 }
1502}
1503
1504fn extract_id_from_sql(sql: &str) -> Option<String> {
1505 let sql_lower = sql.to_lowercase();
1506 let where_idx = sql_lower.find("where")?;
1507 let where_clause = &sql_lower[where_idx + 5..];
1508
1509 let bytes = where_clause.as_bytes();
1510 let mut i = 0;
1511 while i < bytes.len() {
1512 if i + 1 < bytes.len() && &bytes[i..i + 2] == b"id" {
1513 let prev_ok = i == 0 || {
1515 let prev_char = bytes[i - 1] as char;
1516 !prev_char.is_ascii_alphanumeric() && prev_char != '_' && prev_char != '.'
1517 };
1518 let next_ok = i + 2 == bytes.len() || {
1520 let next_char = bytes[i + 2] as char;
1521 !next_char.is_ascii_alphanumeric() && next_char != '_'
1522 };
1523
1524 if prev_ok && next_ok {
1525 let mut j = i + 2;
1528 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1529 j += 1;
1530 }
1531 if j < bytes.len() && bytes[j] == b'=' {
1532 j += 1;
1533 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1534 j += 1;
1535 }
1536 let mut val_str = String::new();
1538 if j < bytes.len() && bytes[j] == b'\'' {
1539 j += 1; while j < bytes.len() && bytes[j] != b'\'' {
1541 val_str.push(bytes[j] as char);
1542 j += 1;
1543 }
1544 return Some(val_str);
1545 }
1546 while j < bytes.len() {
1548 let c = bytes[j] as char;
1549 if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
1550 break;
1551 }
1552 val_str.push(c);
1553 j += 1;
1554 }
1555 if !val_str.is_empty() {
1556 return Some(val_str);
1557 }
1558 }
1559 }
1560 }
1561 i += 1;
1562 }
1563 None
1564}
1565
1566fn sql_result_summary(
1567 operation: SqlLogOperation,
1568 result_count: Option<usize>,
1569 result_type: Option<&str>,
1570 affected_rows: Option<u64>,
1571 debug_sql: &str,
1572) -> String {
1573 match operation {
1574 SqlLogOperation::Select => {
1575 let count = result_count.unwrap_or(0);
1576 match count {
1577 0 => "MISS".to_owned(),
1578 1 => match result_type {
1579 Some(result_type) => extract_id_from_sql(debug_sql)
1580 .map(|id| format!("{result_type}({id})"))
1581 .unwrap_or_else(|| result_type.to_owned()),
1582 None => "row".to_owned(),
1583 },
1584 _ => match result_type {
1585 Some(result_type) => format!("{count}*{result_type}"),
1586 None => format!("{count}*rows"),
1587 },
1588 }
1589 }
1590 _ => {
1591 let affected = affected_rows.unwrap_or(0);
1592 format!("{affected} UPDATED")
1593 }
1594 }
1595}
1596
1597fn pretty_sql(sql: &str) -> String {
1598 let mut pretty = sql.to_owned();
1599 for keyword in [
1600 " FROM ",
1601 " WHERE ",
1602 " GROUP BY ",
1603 " HAVING ",
1604 " ORDER BY ",
1605 " LIMIT ",
1606 " OFFSET ",
1607 " RETURNING ",
1608 ] {
1609 pretty = pretty.replace(keyword, &format!("\n{}", keyword.trim_start()));
1610 }
1611 pretty.replace(" AND ", "\n AND ")
1612}
1613
1614#[cfg(test)]
1615mod sql_log_option_tests {
1616 use super::*;
1617
1618 #[test]
1619 fn disabled_sql_log_rejects_executor_metadata_before_recording() {
1620 let mut context = UserContext::default();
1621 context.disable_sql_log();
1622 let now = SystemTime::now();
1623 context.record_metadata_log(&teaql_data_service::ExecutionMetadata {
1624 backend: "sql".to_owned(),
1625 operation: teaql_data_service::DataServiceOperation::Query,
1626 started_at: now,
1627 ended_at: now,
1628 affected_rows: None,
1629 result_count: Some(1),
1630 trace_chain: Vec::new(),
1631 comment: Some("disabled log test".to_owned()),
1632 backend_request_id: None,
1633 parameterized_query: Some("SELECT id FROM sample WHERE id = $1".to_owned()),
1634 params: vec![Value::I64(1)],
1635 debug_query: Some("SELECT id FROM sample WHERE id = 1".to_owned()),
1636 });
1637 assert!(context.sql_logs().is_empty());
1638 }
1639}
1640
1641#[cfg(test)]
1642mod entity_runtime_state_tests {
1643 use super::*;
1644 use crate::EntityKey;
1645
1646 #[test]
1647 fn reused_user_context_returns_independent_mutation_ledgers() {
1648 let context = UserContext::default();
1649 let first = context.entity_runtime_state();
1650 let key = EntityKey::new("School", 1_u64);
1651 first.set(key.clone(), "name", "First");
1652
1653 let second = context.entity_runtime_state();
1654
1655 assert_eq!(first.changed_field_names(&key).len(), 1);
1656 assert!(second.changed_field_names(&key).is_empty());
1657 }
1658}