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