Skip to main content

teaql_runtime/
context.rs

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, Record, UpdateCommand, Value};
11use teaql_sql::{CompiledQuery, DatabaseKind};
12
13use crate::{
14    CheckObjectStatus, CheckResult, CheckResults, CheckerRegistry, ContextError,
15    EntityDataServiceBehavior, EntityDataServiceBehaviorRegistry, EntityRegistry, GraphNode,
16    InternalIdGenerator, Language, MetadataStore, ObjectLocation, RawAuditEvent, RawAuditEventSink,
17    RequestPolicy, RuntimeError, local_id_generator,
18};
19use crate::{DataServiceError, EntityRoot};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct ContextEntityRef {
23    pub entity_type: String,
24    pub id: u64,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ContextRootError {
29    pub expected_entity_type: String,
30    pub actual_root: Option<ContextEntityRef>,
31}
32
33impl std::fmt::Display for ContextRootError {
34    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match &self.actual_root {
36            None => write!(formatter, "active root {} is missing from UserContext", self.expected_entity_type),
37            Some(actual) => write!(formatter, "active root type is {}, expected {}", actual.entity_type, self.expected_entity_type),
38        }
39    }
40}
41
42impl std::error::Error for ContextRootError {}
43
44#[cfg(test)]
45mod active_root_tests {
46    use super::UserContext;
47
48    #[test]
49    fn active_root_is_typed_and_fails_closed() {
50        let context = UserContext::new().with_active_root("Tenant", 42);
51        assert_eq!(context.require_active_root("Tenant").unwrap().id, 42);
52        assert!(context.require_active_root("Organization").is_err());
53        assert!(UserContext::new().require_active_root("Tenant").is_err());
54    }
55}
56
57#[derive(Debug, Clone, PartialEq)]
58pub struct ContinuousPageCursor {
59    pub cursor_id: String,
60    pub query_key: String,
61    pub entity: String,
62    pub direction: teaql_core::SortDirection,
63    pub boundary: Value,
64    pub page_size: u64,
65    pub next_offset: u64,
66    pub expires_at: SystemTime,
67}
68
69#[async_trait::async_trait]
70pub trait ContinuousPageCursorStore: Send + Sync + 'static {
71    async fn get(
72        &self,
73        query_key: &str,
74        target_offset: u64,
75    ) -> Result<Option<ContinuousPageCursor>, String>;
76    async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String>;
77    async fn invalidate(&self, query_key: &str) -> Result<(), String>;
78}
79
80pub struct InMemoryContinuousPageCursorStore {
81    cursors: Mutex<HashMap<String, ContinuousPageCursor>>,
82    max_entries: usize,
83}
84
85impl Default for InMemoryContinuousPageCursorStore {
86    fn default() -> Self {
87        Self {
88            cursors: Mutex::new(HashMap::new()),
89            max_entries: 4096,
90        }
91    }
92}
93
94#[async_trait::async_trait]
95impl ContinuousPageCursorStore for InMemoryContinuousPageCursorStore {
96    async fn get(
97        &self,
98        query_key: &str,
99        target_offset: u64,
100    ) -> Result<Option<ContinuousPageCursor>, String> {
101        let key = format!("{query_key}:{target_offset}");
102        let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
103        if cursors
104            .get(&key)
105            .is_some_and(|cursor| cursor.expires_at <= SystemTime::now())
106        {
107            cursors.remove(&key);
108        }
109        Ok(cursors.get(&key).cloned())
110    }
111
112    async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String> {
113        let key = format!("{}:{}", cursor.query_key, cursor.next_offset);
114        let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
115        if cursors.len() >= self.max_entries {
116            if let Some(expired_or_oldest) = cursors
117                .iter()
118                .min_by_key(|(_, value)| value.expires_at)
119                .map(|(key, _)| key.clone())
120            {
121                cursors.remove(&expired_or_oldest);
122            }
123        }
124        cursors.insert(key, cursor);
125        Ok(())
126    }
127
128    async fn invalidate(&self, query_key: &str) -> Result<(), String> {
129        let prefix = format!("{query_key}:");
130        self.cursors
131            .lock()
132            .map_err(|e| e.to_string())?
133            .retain(|key, _| !key.starts_with(&prefix));
134        Ok(())
135    }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum SqlLogOperation {
140    Select,
141    Insert,
142    Update,
143    Delete,
144    Recover,
145}
146
147impl SqlLogOperation {
148    pub fn is_select(self) -> bool {
149        matches!(self, Self::Select)
150    }
151
152    pub fn is_mutation(self) -> bool {
153        !self.is_select()
154    }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
158pub struct SqlLogOptions {
159    pub select: bool,
160    pub mutation: bool,
161}
162
163impl SqlLogOptions {
164    pub fn disabled() -> Self {
165        Self {
166            select: false,
167            mutation: false,
168        }
169    }
170
171    pub fn select_only() -> Self {
172        Self {
173            select: true,
174            mutation: false,
175        }
176    }
177
178    pub fn mutation_only() -> Self {
179        Self {
180            select: false,
181            mutation: true,
182        }
183    }
184
185    pub fn all() -> Self {
186        Self {
187            select: true,
188            mutation: true,
189        }
190    }
191
192    pub fn enabled_for(self, operation: SqlLogOperation) -> bool {
193        match operation.is_select() {
194            true => self.select,
195            false => self.mutation,
196        }
197    }
198}
199
200#[derive(Debug, Clone, PartialEq)]
201pub struct SqlLogEntry {
202    pub operation: SqlLogOperation,
203    pub sql: String,
204    pub params: Vec<Value>,
205    pub debug_sql: String,
206    pub pretty_sql: String,
207    pub started_at: SystemTime,
208    pub ended_at: SystemTime,
209    pub elapsed: Duration,
210    pub result_count: Option<usize>,
211    pub result_type: Option<String>,
212    pub affected_rows: Option<u64>,
213    pub result_summary: String,
214}
215
216#[derive(Debug, Clone, PartialEq)]
217pub struct UnifiedLogEntry {
218    pub timestamp: SystemTime,
219    pub user_identifier: Option<String>,
220    pub trace_chain: Vec<teaql_core::TraceNode>,
221    pub payload: LogPayload,
222}
223
224#[derive(Debug, Clone, PartialEq)]
225pub enum LogPayload {
226    Sql(SqlLogEntry),
227    Info(InfoLogEntry),
228}
229
230#[derive(Debug, Clone, PartialEq)]
231pub struct InfoLogEntry {
232    pub message: String,
233}
234
235#[derive(Clone, Default)]
236pub struct UnifiedLogBuffer {
237    pub entries: std::sync::Arc<Mutex<Vec<UnifiedLogEntry>>>,
238}
239
240pub trait SchemaProvider: Send + Sync {
241    fn ensure_schema<'a>(
242        &'a self,
243        context: &'a UserContext,
244    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>>;
245}
246
247pub struct UserContext {
248    active_root: Option<ContextEntityRef>,
249    pub(crate) metadata: Option<Box<dyn MetadataStore>>,
250    pub(crate) entity_registry: Option<Box<dyn EntityRegistry>>,
251    pub(crate) entity_data_service_behavior_registry:
252        Option<Box<dyn EntityDataServiceBehaviorRegistry>>,
253    pub(crate) request_policy: Option<Box<dyn RequestPolicy>>,
254    pub(crate) checker_registry: Option<Box<dyn CheckerRegistry>>,
255    pub(crate) event_sink: Option<Box<dyn RawAuditEventSink>>,
256    pub(crate) custom_event_sink: Option<Box<dyn crate::SafeAuditEventSink>>,
257    pub(crate) internal_id_generator: Option<Box<dyn InternalIdGenerator>>,
258    schema_provider: Option<Box<dyn SchemaProvider>>,
259    language: Language,
260    i18n_catalog: Arc<crate::I18nCatalog>,
261    typed_resources: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
262    named_resources: BTreeMap<String, Box<dyn Any + Send + Sync>>,
263    locals: BTreeMap<String, Value>,
264    pub(crate) initial_graphs: Vec<GraphNode>,
265    entity_root: EntityRoot,
266    sql_log_options: SqlLogOptions,
267    sql_log_entries: Mutex<Vec<SqlLogEntry>>,
268    user_identifier: Option<String>,
269    timezone: Option<String>,
270    trace_id: String,
271    continuous_page_cursor_store: std::sync::Arc<dyn ContinuousPageCursorStore>,
272    continuous_page_observation: Mutex<(String, Option<String>)>,
273    local_lock_owner: u64,
274    remote_lock_owner: String,
275    runtime_telemetry: Arc<dyn crate::RuntimeTelemetry>,
276}
277
278#[derive(Clone, Copy)]
279struct LocalLockEntry {
280    owner: u64,
281    expires_at: Option<Instant>,
282}
283
284#[derive(Default)]
285struct ProcessLocalLocks {
286    entries: Mutex<HashMap<String, LocalLockEntry>>,
287    changed: Condvar,
288}
289
290static PROCESS_LOCAL_LOCKS: OnceLock<ProcessLocalLocks> = OnceLock::new();
291static NEXT_LOCAL_LOCK_OWNER: AtomicU64 = AtomicU64::new(1);
292
293impl Default for UserContext {
294    fn default() -> Self {
295        let pid = std::process::id();
296        let thread_id_str = format!("{:?}", std::thread::current().id());
297        let numeric_thread_id = thread_id_str
298            .strip_prefix("ThreadId(")
299            .and_then(|s| s.strip_suffix(")"))
300            .unwrap_or(&thread_id_str);
301        let os_user = std::env::var("USER")
302            .or_else(|_| std::env::var("USERNAME"))
303            .unwrap_or_else(|_| "main".to_owned());
304        let user_id = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
305        let owner_sequence = NEXT_LOCAL_LOCK_OWNER.fetch_add(1, Ordering::Relaxed);
306        Self {
307            active_root: None,
308            metadata: None,
309            entity_registry: None,
310            entity_data_service_behavior_registry: None,
311            request_policy: None,
312            checker_registry: None,
313            event_sink: None,
314            custom_event_sink: None,
315            internal_id_generator: None,
316            schema_provider: None,
317            language: Language::default(),
318            i18n_catalog: crate::I18nCatalog::builtin().clone(),
319            typed_resources: HashMap::new(),
320            named_resources: BTreeMap::new(),
321            locals: BTreeMap::new(),
322            initial_graphs: Vec::new(),
323            entity_root: EntityRoot::default(),
324            sql_log_options: SqlLogOptions::all(),
325            sql_log_entries: Mutex::new(Vec::new()),
326            user_identifier: Some(user_id),
327            timezone: Some("UTC".to_owned()),
328            trace_id: format!(
329                "req-{pid}-{numeric_thread_id}-{:x}",
330                std::time::SystemTime::now()
331                    .duration_since(std::time::UNIX_EPOCH)
332                    .unwrap_or_default()
333                    .as_micros()
334            ),
335            continuous_page_cursor_store: std::sync::Arc::new(
336                InMemoryContinuousPageCursorStore::default(),
337            ),
338            continuous_page_observation: Mutex::new(("DISABLED".to_owned(), None)),
339            local_lock_owner: owner_sequence,
340            remote_lock_owner: format!(
341                "teaql:{pid}:{owner_sequence}:{}",
342                SystemTime::now()
343                    .duration_since(SystemTime::UNIX_EPOCH)
344                    .unwrap_or_default()
345                    .as_nanos()
346            ),
347            runtime_telemetry: Arc::new(crate::NoopRuntimeTelemetry),
348        }
349    }
350}
351
352#[async_trait::async_trait]
353pub trait DataStore: Send + Sync + 'static {
354    async fn get(&self, key: &str) -> Option<Value>;
355    async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>);
356    async fn remove(&self, key: &str);
357}
358
359/// Provider-neutral distributed lock boundary.
360///
361/// Implementations must associate an acquired lock with `owner_token` and
362/// release it only while that token still owns the key. A zero timeout is one
363/// non-blocking attempt; a zero expiry means no automatic lease expiry.
364#[async_trait::async_trait]
365pub trait RemoteLockProvider: Send + Sync + 'static {
366    async fn try_remote_lock(
367        &self,
368        key: &str,
369        owner_token: &str,
370        timeout_millis: u64,
371        expire_millis: u64,
372    ) -> bool;
373
374    async fn unlock_remote(&self, key: &str, owner_token: &str) -> bool;
375}
376
377#[derive(Default)]
378pub struct InMemoryDataStore {
379    cache: std::sync::RwLock<HashMap<String, (Value, Option<std::time::Instant>)>>,
380}
381
382#[async_trait::async_trait]
383impl DataStore for InMemoryDataStore {
384    async fn get(&self, key: &str) -> Option<Value> {
385        let lock = self.cache.read().unwrap();
386        if let Some((val, expires_at)) = lock.get(key) {
387            if let Some(exp) = expires_at {
388                if std::time::Instant::now() > *exp {
389                    return None;
390                }
391            }
392            return Some(val.clone());
393        }
394        None
395    }
396
397    async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>) {
398        let mut lock = self.cache.write().unwrap();
399        let expires_at = timeout_seconds
400            .map(|secs| std::time::Instant::now() + std::time::Duration::from_secs(secs));
401        lock.insert(key.to_string(), (value, expires_at));
402    }
403
404    async fn remove(&self, key: &str) {
405        let mut lock = self.cache.write().unwrap();
406        lock.remove(key);
407    }
408}
409
410impl UserContext {
411    pub fn new() -> Self {
412        Self::default()
413    }
414
415    pub fn with_active_root(mut self, entity_type: impl Into<String>, id: u64) -> Self {
416        let entity_type = entity_type.into();
417        assert!(!entity_type.trim().is_empty(), "active root entity type is required");
418        assert!(id > 0, "active root id must be positive");
419        self.active_root = Some(ContextEntityRef { entity_type, id });
420        self
421    }
422
423    pub fn require_active_root(&self, expected_entity_type: &str) -> Result<&ContextEntityRef, ContextRootError> {
424        match &self.active_root {
425            Some(root) if root.entity_type == expected_entity_type => Ok(root),
426            actual_root => Err(ContextRootError {
427                expected_entity_type: expected_entity_type.to_owned(),
428                actual_root: actual_root.clone(),
429            }),
430        }
431    }
432
433    pub fn with_runtime_telemetry(mut self, telemetry: Arc<dyn crate::RuntimeTelemetry>) -> Self {
434        self.runtime_telemetry = telemetry;
435        self
436    }
437
438    pub fn set_runtime_telemetry(&mut self, telemetry: Arc<dyn crate::RuntimeTelemetry>) {
439        self.runtime_telemetry = telemetry;
440    }
441
442    pub fn runtime_telemetry(&self) -> &Arc<dyn crate::RuntimeTelemetry> {
443        &self.runtime_telemetry
444    }
445
446    pub fn start_runtime_operation(
447        &self,
448        operation: crate::RuntimeOperation,
449    ) -> crate::FailOpenRuntimeTelemetryScope {
450        crate::start_runtime_operation(&self.runtime_telemetry, operation)
451    }
452
453    pub fn try_local_lock(&self, key: &str, timeout_millis: u64, expire_millis: u64) -> bool {
454        let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
455        let deadline = Instant::now() + Duration::from_millis(timeout_millis);
456        let mut entries = locks.entries.lock().expect("local lock state poisoned");
457        loop {
458            let now = Instant::now();
459            match entries.get(key).copied() {
460                None => {
461                    entries.insert(
462                        key.to_owned(),
463                        LocalLockEntry {
464                            owner: self.local_lock_owner,
465                            expires_at: (expire_millis > 0)
466                                .then(|| now + Duration::from_millis(expire_millis)),
467                        },
468                    );
469                    return true;
470                }
471                Some(current)
472                    if current.owner == self.local_lock_owner
473                        || current.expires_at.is_some_and(|expiry| now >= expiry) =>
474                {
475                    entries.insert(
476                        key.to_owned(),
477                        LocalLockEntry {
478                            owner: self.local_lock_owner,
479                            expires_at: (expire_millis > 0)
480                                .then(|| now + Duration::from_millis(expire_millis)),
481                        },
482                    );
483                    return true;
484                }
485                Some(current) => {
486                    if timeout_millis == 0 || now >= deadline {
487                        return false;
488                    }
489                    let wake_after = current
490                        .expires_at
491                        .map(|expiry| expiry.saturating_duration_since(now))
492                        .unwrap_or_else(|| deadline.saturating_duration_since(now))
493                        .min(deadline.saturating_duration_since(now));
494                    let waited = locks
495                        .changed
496                        .wait_timeout(entries, wake_after)
497                        .expect("local lock state poisoned");
498                    entries = waited.0;
499                }
500            }
501        }
502    }
503
504    pub fn unlock_local(&self, key: &str) {
505        let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
506        let mut entries = locks.entries.lock().expect("local lock state poisoned");
507        if entries
508            .get(key)
509            .is_some_and(|entry| entry.owner == self.local_lock_owner)
510        {
511            entries.remove(key);
512            locks.changed.notify_all();
513        }
514    }
515
516    /// Attempts to acquire a provider-backed distributed lock.
517    ///
518    /// A missing provider remains a no-op success, matching the optional
519    /// Remote Lock boundary in the other TeaQL runtimes. Install an
520    /// `Arc<dyn RemoteLockProvider>` resource to enable distributed exclusion.
521    pub async fn try_remote_lock(
522        &self,
523        key: &str,
524        timeout_millis: u64,
525        expire_millis: u64,
526    ) -> bool {
527        match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
528            Some(provider) => {
529                provider
530                    .try_remote_lock(key, &self.remote_lock_owner, timeout_millis, expire_millis)
531                    .await
532            }
533            None => true,
534        }
535    }
536
537    /// Releases a distributed lock only when this context still owns it.
538    pub async fn unlock_remote(&self, key: &str) -> bool {
539        match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
540            Some(provider) => provider.unlock_remote(key, &self.remote_lock_owner).await,
541            None => true,
542        }
543    }
544
545    pub fn user_identifier(&self) -> Option<&str> {
546        self.user_identifier.as_deref()
547    }
548
549    pub fn set_user_identifier(&mut self, user_identifier: impl Into<String>) {
550        self.user_identifier = Some(user_identifier.into());
551    }
552
553    pub fn set_continuous_page_cursor_store(
554        &mut self,
555        store: std::sync::Arc<dyn ContinuousPageCursorStore>,
556    ) {
557        self.continuous_page_cursor_store = store;
558    }
559
560    pub fn continuous_page_plan(&self) -> Option<String> {
561        self.continuous_page_observation
562            .lock()
563            .ok()
564            .map(|value| value.0.clone())
565    }
566
567    pub fn continuous_page_cursor_id(&self) -> Option<String> {
568        self.continuous_page_observation
569            .lock()
570            .ok()
571            .and_then(|value| value.1.clone())
572    }
573
574    pub(crate) fn observe_continuous_page(
575        &self,
576        plan: impl Into<String>,
577        cursor_id: Option<String>,
578    ) {
579        if let Ok(mut observation) = self.continuous_page_observation.lock() {
580            *observation = (plan.into(), cursor_id);
581        }
582    }
583
584    pub(crate) fn continuous_page_cursor_store(&self) -> &dyn ContinuousPageCursorStore {
585        self.continuous_page_cursor_store.as_ref()
586    }
587
588    pub fn with_user_identifier(mut self, user_identifier: impl Into<String>) -> Self {
589        self.user_identifier = Some(user_identifier.into());
590        self
591    }
592
593    pub fn set_user_identifier_option(&mut self, user_identifier: Option<String>) {
594        self.user_identifier = user_identifier;
595    }
596
597    pub fn with_user_identifier_option(mut self, user_identifier: Option<String>) -> Self {
598        self.user_identifier = user_identifier;
599        self
600    }
601
602    pub fn timezone(&self) -> Option<&str> {
603        self.timezone.as_deref()
604    }
605
606    pub fn set_timezone(&mut self, timezone: impl Into<String>) {
607        self.timezone = Some(timezone.into());
608    }
609
610    pub fn with_timezone(mut self, timezone: impl Into<String>) -> Self {
611        self.timezone = Some(timezone.into());
612        self
613    }
614
615    pub fn trace_id(&self) -> &str {
616        &self.trace_id
617    }
618
619    pub fn set_trace_id(&mut self, trace_id: impl Into<String>) {
620        self.trace_id = trace_id.into();
621    }
622
623    pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
624        self.trace_id = trace_id.into();
625        self
626    }
627
628    pub fn with_module(mut self, module: crate::RuntimeModule) -> Self {
629        module.apply_to(&mut self);
630        self
631    }
632
633    pub fn entity_root(&self) -> EntityRoot {
634        self.entity_root.clone()
635    }
636
637    pub fn initial_graphs(&self) -> &[GraphNode] {
638        &self.initial_graphs
639    }
640
641    pub fn set_initial_graphs(&mut self, graphs: Vec<GraphNode>) {
642        self.initial_graphs = graphs;
643    }
644
645    pub fn with_metadata(mut self, metadata: impl MetadataStore + 'static) -> Self {
646        self.metadata = Some(Box::new(metadata));
647        self
648    }
649
650    pub fn set_metadata(&mut self, metadata: impl MetadataStore + 'static) {
651        self.metadata = Some(Box::new(metadata));
652    }
653
654    pub fn with_entity_registry(mut self, registry: impl EntityRegistry + 'static) -> Self {
655        self.entity_registry = Some(Box::new(registry));
656        self
657    }
658
659    pub fn set_entity_registry(&mut self, registry: impl EntityRegistry + 'static) {
660        self.entity_registry = Some(Box::new(registry));
661    }
662
663    pub fn with_entity_data_service_behavior_registry(
664        mut self,
665        registry: impl EntityDataServiceBehaviorRegistry + 'static,
666    ) -> Self {
667        self.entity_data_service_behavior_registry = Some(Box::new(registry));
668        self
669    }
670
671    pub fn set_entity_data_service_behavior_registry(
672        &mut self,
673        registry: impl EntityDataServiceBehaviorRegistry + 'static,
674    ) {
675        self.entity_data_service_behavior_registry = Some(Box::new(registry));
676    }
677
678    pub fn with_request_policy(mut self, policy: impl RequestPolicy + 'static) -> Self {
679        self.request_policy = Some(Box::new(policy));
680        self
681    }
682
683    pub fn set_request_policy(&mut self, policy: impl RequestPolicy + 'static) {
684        self.request_policy = Some(Box::new(policy));
685    }
686
687    pub fn clear_request_policy(&mut self) {
688        self.request_policy = None;
689    }
690
691    pub fn with_checker_registry(mut self, registry: impl CheckerRegistry + 'static) -> Self {
692        self.checker_registry = Some(Box::new(registry));
693        self
694    }
695
696    pub fn set_checker_registry(&mut self, registry: impl CheckerRegistry + 'static) {
697        self.checker_registry = Some(Box::new(registry));
698    }
699
700    pub(crate) fn with_event_sink(mut self, sink: impl RawAuditEventSink + 'static) -> Self {
701        self.event_sink = Some(Box::new(sink));
702        self
703    }
704
705    pub(crate) fn set_event_sink(&mut self, sink: impl RawAuditEventSink + 'static) {
706        self.event_sink = Some(Box::new(sink));
707    }
708
709    pub fn with_custom_event_sink(
710        mut self,
711        sink: impl crate::SafeAuditEventSink + 'static,
712    ) -> Self {
713        self.custom_event_sink = Some(Box::new(sink));
714        self
715    }
716
717    pub fn set_custom_event_sink(&mut self, sink: impl crate::SafeAuditEventSink + 'static) {
718        self.custom_event_sink = Some(Box::new(sink));
719    }
720
721    pub fn with_internal_id_generator(
722        mut self,
723        generator: impl InternalIdGenerator + 'static,
724    ) -> Self {
725        self.internal_id_generator = Some(Box::new(generator));
726        self
727    }
728
729    pub fn set_internal_id_generator(&mut self, generator: impl InternalIdGenerator + 'static) {
730        self.internal_id_generator = Some(Box::new(generator));
731    }
732
733    pub fn with_schema_provider(mut self, provider: impl SchemaProvider + 'static) -> Self {
734        self.schema_provider = Some(Box::new(provider));
735        self
736    }
737
738    pub fn set_schema_provider(&mut self, provider: impl SchemaProvider + 'static) {
739        self.schema_provider = Some(Box::new(provider));
740    }
741
742    pub async fn ensure_schema(&self) -> Result<(), RuntimeError> {
743        let provider = self
744            .schema_provider
745            .as_ref()
746            .ok_or_else(|| RuntimeError::Schema("missing schema provider".to_owned()))?;
747        provider.ensure_schema(self).await
748    }
749
750    pub fn with_language(mut self, language: Language) -> Self {
751        self.language = language;
752        self
753    }
754
755    pub fn set_language(&mut self, language: Language) {
756        self.language = language;
757    }
758
759    pub fn with_i18n_catalog(mut self, catalog: Arc<crate::I18nCatalog>) -> Self {
760        self.i18n_catalog = catalog;
761        self
762    }
763
764    pub fn set_i18n_catalog(&mut self, catalog: Arc<crate::I18nCatalog>) {
765        self.i18n_catalog = catalog;
766    }
767
768    pub fn with_sql_log_options(mut self, options: SqlLogOptions) -> Self {
769        self.sql_log_options = options;
770        self
771    }
772
773    pub fn set_sql_log_options(&mut self, options: SqlLogOptions) {
774        self.sql_log_options = options;
775    }
776
777    pub fn enable_select_sql_log(&mut self) {
778        self.sql_log_options.select = true;
779    }
780
781    pub fn enable_mutation_sql_log(&mut self) {
782        self.sql_log_options.mutation = true;
783    }
784
785    pub fn enable_all_sql_log(&mut self) {
786        self.sql_log_options = SqlLogOptions::all();
787    }
788
789    pub fn disable_sql_log(&mut self) {
790        self.sql_log_options = SqlLogOptions::disabled();
791        self.clear_sql_logs();
792    }
793
794    pub fn sql_log_options(&self) -> SqlLogOptions {
795        self.sql_log_options
796    }
797
798    pub fn sql_logs(&self) -> Vec<SqlLogEntry> {
799        self.sql_log_entries
800            .lock()
801            .map(|entries| entries.clone())
802            .unwrap_or_default()
803    }
804
805    pub fn clear_sql_logs(&self) {
806        if let Ok(mut entries) = self.sql_log_entries.lock() {
807            entries.clear();
808        }
809    }
810
811    pub(crate) fn record_sql_log(
812        &self,
813        operation: SqlLogOperation,
814        query: &CompiledQuery,
815        database_kind: DatabaseKind,
816        started_at: SystemTime,
817        ended_at: SystemTime,
818        elapsed: Duration,
819        result_count: Option<usize>,
820        result_type: Option<String>,
821        affected_rows: Option<u64>,
822        trace_chain: Vec<teaql_core::TraceNode>,
823    ) {
824        if !self.sql_log_options.enabled_for(operation) {
825            return;
826        }
827        let debug_sql = query.debug_sql(database_kind);
828        let result_summary = sql_result_summary(
829            operation,
830            result_count,
831            result_type.as_deref(),
832            affected_rows,
833            &debug_sql,
834        );
835
836        let sql_log_entry = SqlLogEntry {
837            operation,
838            sql: query.sql.clone(),
839            params: query.params.clone(),
840            pretty_sql: pretty_sql(&debug_sql),
841            debug_sql: debug_sql.clone(),
842            started_at,
843            ended_at,
844            elapsed,
845            result_summary: result_summary.clone(),
846            result_count,
847            result_type,
848            affected_rows,
849        };
850
851        if let Ok(mut entries) = self.sql_log_entries.lock() {
852            // Keep sql_log_entries backwards-compatible for now if needed,
853            // wait, we modified SqlLogEntry. We can just push it directly since we removed comment.
854            // Wait, we need to push a cloned SqlLogEntry since it doesn't have comment.
855            entries.push(sql_log_entry.clone());
856        }
857
858        if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
859            if let Ok(mut entries) = buf.entries.lock() {
860                entries.push(UnifiedLogEntry {
861                    timestamp: started_at,
862                    user_identifier: self.user_identifier.clone(),
863                    trace_chain: trace_chain.clone(),
864                    payload: LogPayload::Sql(sql_log_entry.clone()),
865                });
866            }
867        }
868
869        crate::log_formatter::LogManager::write_sql_log(&trace_chain, &sql_log_entry);
870    }
871
872    pub(crate) fn record_metadata_log(&self, metadata: &teaql_data_service::ExecutionMetadata) {
873        if let Some(debug_sql) = &metadata.debug_query {
874            let sql_log_entry = SqlLogEntry {
875                operation: match metadata.operation {
876                    teaql_data_service::DataServiceOperation::Query => SqlLogOperation::Select,
877                    teaql_data_service::DataServiceOperation::Insert => SqlLogOperation::Insert,
878                    teaql_data_service::DataServiceOperation::Update => SqlLogOperation::Update,
879                    teaql_data_service::DataServiceOperation::Delete => SqlLogOperation::Delete,
880                    teaql_data_service::DataServiceOperation::Recover => SqlLogOperation::Update, // Approximate
881                    teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
882                    teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
883                },
884                sql: metadata.parameterized_query.clone().unwrap_or_default(),
885                params: metadata.params.clone(),
886                pretty_sql: pretty_sql(debug_sql),
887                debug_sql: debug_sql.clone(),
888                started_at: metadata.started_at,
889                ended_at: metadata.ended_at,
890                elapsed: metadata
891                    .ended_at
892                    .duration_since(metadata.started_at)
893                    .unwrap_or_default(),
894                result_count: metadata.result_count,
895                result_type: None, // Not directly available
896                affected_rows: metadata.affected_rows,
897                result_summary: String::new(), // We can synthesize this if needed, or leave it empty/basic
898            };
899
900            // synthesize a summary for the log
901            let mut summary = String::new();
902            if let Some(c) = metadata.result_count {
903                summary = format!("{} rows returned", c);
904            } else if let Some(a) = metadata.affected_rows {
905                summary = format!("{} rows affected", a);
906            }
907
908            let mut final_entry = sql_log_entry;
909            final_entry.result_summary = summary;
910
911            if let Ok(mut entries) = self.sql_log_entries.lock() {
912                entries.push(final_entry.clone());
913            }
914
915            if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
916                if let Ok(mut entries) = buf.entries.lock() {
917                    entries.push(UnifiedLogEntry {
918                        timestamp: metadata.started_at,
919                        user_identifier: self.user_identifier.clone(),
920                        trace_chain: metadata.trace_chain.clone(),
921                        payload: LogPayload::Sql(final_entry.clone()),
922                    });
923                }
924            }
925
926            crate::log_formatter::LogManager::write_sql_log(&metadata.trace_chain, &final_entry);
927        }
928    }
929
930    pub fn language(&self) -> Language {
931        self.language
932    }
933
934    pub fn set_language_code(&mut self, code: &str) -> Result<(), RuntimeError> {
935        let Some(language) = Language::from_code(code) else {
936            return Err(RuntimeError::UnsupportedLocale(code.to_owned()));
937        };
938        self.language = language;
939        Ok(())
940    }
941
942    pub fn set_locale_code(&mut self, code: &str) -> Result<(), RuntimeError> {
943        self.set_language_code(code)
944    }
945
946    pub fn generate_id(&self, entity: &str) -> Result<Option<u64>, RuntimeError> {
947        self.internal_id_generator
948            .as_ref()
949            .map(|generator| generator.generate_id(entity))
950            .transpose()
951    }
952
953    pub fn next_id(&self, entity: &str) -> Result<u64, RuntimeError> {
954        match self.generate_id(entity)? {
955            Some(id) => Ok(id),
956            None => local_id_generator().generate_id(entity),
957        }
958    }
959
960    pub fn entity(&self, name: &str) -> Option<&EntityDescriptor> {
961        self.metadata
962            .as_ref()
963            .and_then(|metadata| metadata.entity(name))
964    }
965
966    pub fn all_entities(&self) -> Vec<&EntityDescriptor> {
967        self.metadata
968            .as_ref()
969            .map(|metadata| metadata.all_entities())
970            .unwrap_or_default()
971    }
972
973    pub fn require_entity(&self, name: &str) -> Result<&EntityDescriptor, RuntimeError> {
974        self.entity(name)
975            .ok_or_else(|| RuntimeError::MissingEntity(name.to_owned()))
976    }
977
978    pub fn insert_resource<T>(&mut self, resource: T)
979    where
980        T: Send + Sync + 'static,
981    {
982        self.typed_resources
983            .insert(TypeId::of::<T>(), Box::new(resource));
984    }
985
986    pub fn get_resource<T>(&self) -> Option<&T>
987    where
988        T: Send + Sync + 'static,
989    {
990        self.typed_resources
991            .get(&TypeId::of::<T>())
992            .and_then(|value| value.downcast_ref::<T>())
993    }
994
995    pub fn require_resource<T>(&self) -> Result<&T, ContextError>
996    where
997        T: Send + Sync + 'static,
998    {
999        self.get_resource::<T>()
1000            .ok_or(ContextError::MissingTypedResource(
1001                std::any::type_name::<T>(),
1002            ))
1003    }
1004
1005    pub fn insert_named_resource<T>(&mut self, name: impl Into<String>, resource: T)
1006    where
1007        T: Send + Sync + 'static,
1008    {
1009        self.named_resources.insert(name.into(), Box::new(resource));
1010    }
1011
1012    pub fn get_named_resource<T>(&self, name: &str) -> Option<&T>
1013    where
1014        T: Send + Sync + 'static,
1015    {
1016        self.named_resources
1017            .get(name)
1018            .and_then(|value| value.downcast_ref::<T>())
1019    }
1020
1021    pub fn require_named_resource<T>(&self, name: &str) -> Result<&T, ContextError>
1022    where
1023        T: Send + Sync + 'static,
1024    {
1025        self.get_named_resource::<T>(name)
1026            .ok_or_else(|| ContextError::MissingResource(name.to_owned()))
1027    }
1028
1029    pub fn put_local(&mut self, key: impl Into<String>, value: impl Into<Value>) {
1030        self.locals.insert(key.into(), value.into());
1031    }
1032
1033    pub fn local(&self, key: &str) -> Option<&Value> {
1034        self.locals.get(key)
1035    }
1036
1037    pub fn remove_local(&mut self, key: &str) -> Option<Value> {
1038        self.locals.remove(key)
1039    }
1040
1041    pub fn has_entity_data_service(&self, entity: &str) -> bool {
1042        let in_registry = self
1043            .entity_registry
1044            .as_ref()
1045            .map(|registry| registry.contains(entity))
1046            .unwrap_or(false);
1047        in_registry || self.entity(entity).is_some()
1048    }
1049
1050    pub fn entity_data_service_behavior(
1051        &self,
1052        entity: &str,
1053    ) -> Option<std::sync::Arc<dyn EntityDataServiceBehavior>> {
1054        self.entity_data_service_behavior_registry
1055            .as_ref()
1056            .and_then(|registry| registry.behavior(entity))
1057    }
1058
1059    pub fn has_checker(&self, entity: &str) -> bool {
1060        self.checker_registry
1061            .as_ref()
1062            .and_then(|registry| registry.checker(entity))
1063            .is_some()
1064    }
1065
1066    pub fn check_and_fix_record(
1067        &self,
1068        entity: &str,
1069        record: &mut Record,
1070    ) -> Result<(), RuntimeError> {
1071        self.check_and_fix_record_at(entity, record, &ObjectLocation::root())
1072    }
1073
1074    pub fn check_and_fix_record_at(
1075        &self,
1076        entity: &str,
1077        record: &mut Record,
1078        location: &ObjectLocation,
1079    ) -> Result<(), RuntimeError> {
1080        let status = CheckObjectStatus::from_record(record);
1081        let checker = self
1082            .checker_registry
1083            .as_ref()
1084            .and_then(|registry| registry.checker(entity));
1085        let mut results = CheckResults::new();
1086        if let Some(checker) = checker {
1087            checker.check_and_fix(self, record, location, &mut results);
1088        }
1089
1090        // Keep runtime validation aligned with the schema generated from the
1091        // same metadata. Custom checkers get the first chance to supply or fix
1092        // a value; afterwards every NOT NULL property must be present on a
1093        // create, and an update must not explicitly clear one.
1094        if let Some(descriptor) = self
1095            .metadata
1096            .as_ref()
1097            .and_then(|metadata| metadata.entity(entity))
1098        {
1099            for property in descriptor
1100                .properties
1101                .iter()
1102                .filter(|property| !property.nullable)
1103            {
1104                let missing = !record.contains_key(&property.name);
1105                let null = matches!(record.get(&property.name), Some(Value::Null));
1106                let property_location = location.clone().member(&property.name);
1107                let already_reported = results.iter().any(|result| {
1108                    result.rule == crate::CheckRule::Required
1109                        && result.location == property_location
1110                });
1111                if ((status.is_create() && missing) || null) && !already_reported {
1112                    results.push(CheckResult::required(property_location));
1113                }
1114            }
1115        }
1116        if results.is_empty() {
1117            return Ok(());
1118        }
1119        self.translate_check_results(&mut results);
1120        Err(RuntimeError::Check(results))
1121    }
1122
1123    pub fn translate_check_results(&self, results: &mut CheckResults) {
1124        for result in results {
1125            result.message = Some(
1126                self.i18n_catalog
1127                    .translate_check_result(self.language, result),
1128            );
1129        }
1130    }
1131
1132    pub fn send_event(&self, event: RawAuditEvent) -> Result<(), RuntimeError> {
1133        let scope = self.start_runtime_operation(
1134            crate::RuntimeOperation::new("audit", format!("{}.event", event.entity))
1135                .attribute("teaql.entity.type", event.entity.clone()),
1136        );
1137        let result = self.send_event_inner(event);
1138        match &result {
1139            Ok(()) => scope.success(std::collections::BTreeMap::new()),
1140            Err(_) => scope.failure("audit_error"),
1141        }
1142        result
1143    }
1144
1145    fn send_event_inner(&self, event: RawAuditEvent) -> Result<(), RuntimeError> {
1146        if let Some(sink) = self.event_sink.as_ref() {
1147            sink.on_event(self, &event)?;
1148        }
1149        if let Some(sink) = self.custom_event_sink.as_ref() {
1150            let (mask_fields, max_len) = self
1151                .metadata
1152                .as_ref()
1153                .and_then(|metadata| metadata.entity(&event.entity))
1154                .map(|desc| (desc.audit_mask_fields.clone(), desc.audit_value_max_len))
1155                .unwrap_or_else(|| (vec![], None));
1156
1157            let safe_event = event.build_safe_event(&mask_fields, max_len);
1158            sink.on_safe_event(self, &safe_event)?;
1159        }
1160
1161        crate::log_formatter::LogManager::write_audit_log(&event);
1162
1163        Ok(())
1164    }
1165
1166    pub(crate) async fn commit_changes_internal<E>(&self) -> Result<(), DataServiceError<E::Error>>
1167    where
1168        E: teaql_data_service::MutationExecutor + Send + Sync + 'static,
1169    {
1170        let executor = self.require_resource::<E>().map_err(|err| {
1171            DataServiceError::Runtime(RuntimeError::Graph(format!(
1172                "cannot commit changes without executor: {err}"
1173            )))
1174        })?;
1175        let change_set = self.entity_root.current_change_set();
1176
1177        for (key, changes) in change_set.changes() {
1178            if changes.is_empty() {
1179                continue;
1180            }
1181            let _entity = self
1182                .require_entity(&key.entity)
1183                .map_err(DataServiceError::Runtime)?;
1184            let mut command = UpdateCommand::new(&key.entity, key.id.clone());
1185            for (field, value) in changes {
1186                command = command.value(field.clone(), value.clone());
1187            }
1188            let request = teaql_data_service::MutationRequest::Update(command);
1189            executor
1190                .mutate(request)
1191                .await
1192                .map_err(DataServiceError::Executor)?;
1193        }
1194
1195        self.entity_root.clear_current_change_set();
1196        Ok(())
1197    }
1198
1199    pub async fn get_in_store(&self, key: &str) -> Option<Value> {
1200        let store = self.get_resource::<Box<dyn DataStore>>()?;
1201        store.get(key).await
1202    }
1203
1204    pub async fn put_in_store(
1205        &self,
1206        key: &str,
1207        value: impl Into<Value>,
1208        timeout_seconds: Option<u64>,
1209    ) {
1210        if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1211            store.put(key, value.into(), timeout_seconds).await;
1212        }
1213    }
1214
1215    pub async fn clear_in_store(&self, key: &str) {
1216        if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1217            store.remove(key).await;
1218        }
1219    }
1220}
1221
1222fn extract_id_from_sql(sql: &str) -> Option<String> {
1223    let sql_lower = sql.to_lowercase();
1224    let where_idx = sql_lower.find("where")?;
1225    let where_clause = &sql_lower[where_idx + 5..];
1226
1227    let bytes = where_clause.as_bytes();
1228    let mut i = 0;
1229    while i < bytes.len() {
1230        if i + 1 < bytes.len() && &bytes[i..i + 2] == b"id" {
1231            // Check boundary before
1232            let prev_ok = i == 0 || {
1233                let prev_char = bytes[i - 1] as char;
1234                !prev_char.is_ascii_alphanumeric() && prev_char != '_' && prev_char != '.'
1235            };
1236            // Check boundary after
1237            let next_ok = i + 2 == bytes.len() || {
1238                let next_char = bytes[i + 2] as char;
1239                !next_char.is_ascii_alphanumeric() && next_char != '_'
1240            };
1241
1242            if prev_ok && next_ok {
1243                // Found the standalone "id" word!
1244                // Now look for "=" after it
1245                let mut j = i + 2;
1246                while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1247                    j += 1;
1248                }
1249                if j < bytes.len() && bytes[j] == b'=' {
1250                    j += 1;
1251                    while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1252                        j += 1;
1253                    }
1254                    // Now extract the value
1255                    let mut val_str = String::new();
1256                    if j < bytes.len() && bytes[j] == b'\'' {
1257                        j += 1; // consume single quote
1258                        while j < bytes.len() && bytes[j] != b'\'' {
1259                            val_str.push(bytes[j] as char);
1260                            j += 1;
1261                        }
1262                        return Some(val_str);
1263                    }
1264                    // No else needed — falls through to unquoted parsing
1265                    while j < bytes.len() {
1266                        let c = bytes[j] as char;
1267                        if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
1268                            break;
1269                        }
1270                        val_str.push(c);
1271                        j += 1;
1272                    }
1273                    if !val_str.is_empty() {
1274                        return Some(val_str);
1275                    }
1276                }
1277            }
1278        }
1279        i += 1;
1280    }
1281    None
1282}
1283
1284fn sql_result_summary(
1285    operation: SqlLogOperation,
1286    result_count: Option<usize>,
1287    result_type: Option<&str>,
1288    affected_rows: Option<u64>,
1289    debug_sql: &str,
1290) -> String {
1291    match operation {
1292        SqlLogOperation::Select => {
1293            let count = result_count.unwrap_or(0);
1294            match count {
1295                0 => "MISS".to_owned(),
1296                1 => match result_type {
1297                    Some(result_type) => extract_id_from_sql(debug_sql)
1298                        .map(|id| format!("{result_type}({id})"))
1299                        .unwrap_or_else(|| result_type.to_owned()),
1300                    None => "row".to_owned(),
1301                },
1302                _ => match result_type {
1303                    Some(result_type) => format!("{count}*{result_type}"),
1304                    None => format!("{count}*rows"),
1305                },
1306            }
1307        }
1308        _ => {
1309            let affected = affected_rows.unwrap_or(0);
1310            format!("{affected} UPDATED")
1311        }
1312    }
1313}
1314
1315fn pretty_sql(sql: &str) -> String {
1316    let mut pretty = sql.to_owned();
1317    for keyword in [
1318        " FROM ",
1319        " WHERE ",
1320        " GROUP BY ",
1321        " HAVING ",
1322        " ORDER BY ",
1323        " LIMIT ",
1324        " OFFSET ",
1325        " RETURNING ",
1326    ] {
1327        pretty = pretty.replace(keyword, &format!("\n{}", keyword.trim_start()));
1328    }
1329    pretty.replace(" AND ", "\n  AND ")
1330}