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, Value};
11use teaql_sql::{CompiledQuery, DatabaseKind};
12
13use crate::EntityRuntimeState;
14use crate::{
15    CheckObjectStatus, CheckResult, CheckResults, CheckerRegistry, ContextError,
16    EntityDataServiceBehavior, EntityDataServiceBehaviorRegistry, EntityGraphBuilder,
17    EntityRegistry, GraphNode, InMemoryEntityGraphDecoderRegistry, InternalIdGenerator, Language,
18    MetadataStore, ObjectLocation, RawAuditEvent, RawAuditEventSink, RequestPolicy, RuntimeError,
19    local_id_generator,
20};
21
22tokio::task_local! {
23    static GENERATED_SCHEMA_BOOTSTRAP_MODE: ();
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ContextEntityRef {
28    pub entity_type: String,
29    pub id: u64,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ContextRootError {
34    pub expected_entity_type: String,
35    pub actual_root: Option<ContextEntityRef>,
36}
37
38impl std::fmt::Display for ContextRootError {
39    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match &self.actual_root {
41            None => write!(
42                formatter,
43                "active root {} is missing from UserContext",
44                self.expected_entity_type
45            ),
46            Some(actual) => write!(
47                formatter,
48                "active root type is {}, expected {}",
49                actual.entity_type, self.expected_entity_type
50            ),
51        }
52    }
53}
54
55impl std::error::Error for ContextRootError {}
56
57#[cfg(test)]
58mod active_root_tests {
59    use super::UserContext;
60
61    #[test]
62    fn active_root_is_typed_and_fails_closed() {
63        let context = UserContext::new().with_active_root("Tenant", 42);
64        assert_eq!(context.require_active_root("Tenant").unwrap().id, 42);
65        assert!(context.require_active_root("Organization").is_err());
66        assert!(UserContext::new().require_active_root("Tenant").is_err());
67    }
68}
69
70#[derive(Debug, Clone, PartialEq)]
71pub struct ContinuousPageCursor {
72    pub cursor_id: String,
73    pub query_key: String,
74    pub entity: String,
75    pub direction: teaql_core::SortDirection,
76    pub boundary: Value,
77    pub page_size: u64,
78    pub next_offset: u64,
79    pub expires_at: SystemTime,
80}
81
82#[async_trait::async_trait]
83pub trait ContinuousPageCursorStore: Send + Sync + 'static {
84    async fn get(
85        &self,
86        query_key: &str,
87        target_offset: u64,
88    ) -> Result<Option<ContinuousPageCursor>, String>;
89    async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String>;
90    async fn invalidate(&self, query_key: &str) -> Result<(), String>;
91}
92
93pub struct InMemoryContinuousPageCursorStore {
94    cursors: Mutex<HashMap<String, ContinuousPageCursor>>,
95    max_entries: usize,
96}
97
98#[derive(Debug, Clone)]
99pub struct RetainedIdSet {
100    pub query_key: String,
101    pub ids: Arc<Vec<u64>>,
102    pub expires_at: SystemTime,
103}
104
105#[async_trait::async_trait]
106pub trait IdSetStore: Send + Sync + 'static {
107    async fn get(&self, query_key: &str) -> Result<Option<RetainedIdSet>, String>;
108    async fn put(&self, id_set: RetainedIdSet) -> Result<(), String>;
109    async fn invalidate(&self, query_key: &str) -> Result<(), String>;
110}
111
112pub struct InMemoryIdSetStore {
113    sets: Mutex<HashMap<String, RetainedIdSet>>,
114    max_entries: usize,
115    max_bytes: usize,
116}
117
118impl Default for InMemoryIdSetStore {
119    fn default() -> Self {
120        Self {
121            sets: Mutex::new(HashMap::new()),
122            max_entries: 64,
123            max_bytes: 256 * 1024 * 1024,
124        }
125    }
126}
127
128impl InMemoryIdSetStore {
129    fn retained_bytes(sets: &HashMap<String, RetainedIdSet>) -> usize {
130        sets.values()
131            .map(|value| value.ids.len().saturating_mul(std::mem::size_of::<u64>()))
132            .sum()
133    }
134}
135
136#[async_trait::async_trait]
137impl IdSetStore for InMemoryIdSetStore {
138    async fn get(&self, query_key: &str) -> Result<Option<RetainedIdSet>, String> {
139        let mut sets = self.sets.lock().map_err(|error| error.to_string())?;
140        if sets
141            .get(query_key)
142            .is_some_and(|value| value.expires_at <= SystemTime::now())
143        {
144            sets.remove(query_key);
145        }
146        Ok(sets.get(query_key).cloned())
147    }
148
149    async fn put(&self, id_set: RetainedIdSet) -> Result<(), String> {
150        let incoming_bytes = id_set.ids.len().saturating_mul(std::mem::size_of::<u64>());
151        if incoming_bytes > self.max_bytes {
152            return Err("ID set exceeds the process-local store memory ceiling".to_owned());
153        }
154        let mut sets = self.sets.lock().map_err(|error| error.to_string())?;
155        sets.retain(|_, value| value.expires_at > SystemTime::now());
156        while sets.len() >= self.max_entries
157            || Self::retained_bytes(&sets).saturating_add(incoming_bytes) > self.max_bytes
158        {
159            let Some(oldest) = sets
160                .iter()
161                .min_by_key(|(_, value)| value.expires_at)
162                .map(|(key, _)| key.clone())
163            else {
164                break;
165            };
166            sets.remove(&oldest);
167        }
168        sets.insert(id_set.query_key.clone(), id_set);
169        Ok(())
170    }
171
172    async fn invalidate(&self, query_key: &str) -> Result<(), String> {
173        self.sets
174            .lock()
175            .map_err(|error| error.to_string())?
176            .remove(query_key);
177        Ok(())
178    }
179}
180
181fn id_set_build_lock(query_key: &str) -> Arc<futures_util::lock::Mutex<()>> {
182    static LOCKS: OnceLock<Mutex<HashMap<String, std::sync::Weak<futures_util::lock::Mutex<()>>>>> =
183        OnceLock::new();
184    let mut locks = LOCKS
185        .get_or_init(|| Mutex::new(HashMap::new()))
186        .lock()
187        .expect("ID set build lock registry poisoned");
188    locks.retain(|_, lock| lock.strong_count() > 0);
189    if let Some(lock) = locks.get(query_key).and_then(std::sync::Weak::upgrade) {
190        return lock;
191    }
192    let lock = Arc::new(futures_util::lock::Mutex::new(()));
193    locks.insert(query_key.to_owned(), Arc::downgrade(&lock));
194    lock
195}
196
197impl Default for InMemoryContinuousPageCursorStore {
198    fn default() -> Self {
199        Self {
200            cursors: Mutex::new(HashMap::new()),
201            max_entries: 4096,
202        }
203    }
204}
205
206#[async_trait::async_trait]
207impl ContinuousPageCursorStore for InMemoryContinuousPageCursorStore {
208    async fn get(
209        &self,
210        query_key: &str,
211        target_offset: u64,
212    ) -> Result<Option<ContinuousPageCursor>, String> {
213        let key = format!("{query_key}:{target_offset}");
214        let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
215        if cursors
216            .get(&key)
217            .is_some_and(|cursor| cursor.expires_at <= SystemTime::now())
218        {
219            cursors.remove(&key);
220        }
221        Ok(cursors.get(&key).cloned())
222    }
223
224    async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String> {
225        let key = format!("{}:{}", cursor.query_key, cursor.next_offset);
226        let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
227        if cursors.len() >= self.max_entries {
228            if let Some(expired_or_oldest) = cursors
229                .iter()
230                .min_by_key(|(_, value)| value.expires_at)
231                .map(|(key, _)| key.clone())
232            {
233                cursors.remove(&expired_or_oldest);
234            }
235        }
236        cursors.insert(key, cursor);
237        Ok(())
238    }
239
240    async fn invalidate(&self, query_key: &str) -> Result<(), String> {
241        let prefix = format!("{query_key}:");
242        self.cursors
243            .lock()
244            .map_err(|e| e.to_string())?
245            .retain(|key, _| !key.starts_with(&prefix));
246        Ok(())
247    }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum SqlLogOperation {
252    Select,
253    Insert,
254    Update,
255    Delete,
256    Recover,
257}
258
259impl SqlLogOperation {
260    pub fn is_select(self) -> bool {
261        matches!(self, Self::Select)
262    }
263
264    pub fn is_mutation(self) -> bool {
265        !self.is_select()
266    }
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub struct SqlLogOptions {
271    pub select: bool,
272    pub mutation: bool,
273}
274
275impl Default for SqlLogOptions {
276    fn default() -> Self {
277        Self::all()
278    }
279}
280
281impl SqlLogOptions {
282    pub fn disabled() -> Self {
283        Self {
284            select: false,
285            mutation: false,
286        }
287    }
288
289    pub fn select_only() -> Self {
290        Self {
291            select: true,
292            mutation: false,
293        }
294    }
295
296    pub fn mutation_only() -> Self {
297        Self {
298            select: false,
299            mutation: true,
300        }
301    }
302
303    pub fn all() -> Self {
304        Self {
305            select: true,
306            mutation: true,
307        }
308    }
309
310    pub fn enabled_for(self, operation: SqlLogOperation) -> bool {
311        match operation.is_select() {
312            true => self.select,
313            false => self.mutation,
314        }
315    }
316}
317
318#[derive(Debug, Clone, PartialEq)]
319pub struct SqlLogEntry {
320    pub operation: SqlLogOperation,
321    pub comment: Option<String>,
322    pub purpose: Option<String>,
323    pub audit_reason: Option<String>,
324    pub trace_path: Vec<teaql_core::TraceNode>,
325    pub sql: String,
326    pub params: Vec<Value>,
327    pub debug_sql: String,
328    pub pretty_sql: String,
329    pub started_at: SystemTime,
330    pub ended_at: SystemTime,
331    pub elapsed: Duration,
332    pub result_count: Option<usize>,
333    pub result_type: Option<String>,
334    pub affected_rows: Option<u64>,
335    pub result_summary: String,
336}
337
338#[derive(Debug, Clone, PartialEq)]
339pub struct UnifiedLogEntry {
340    pub timestamp: SystemTime,
341    pub user_identifier: Option<String>,
342    pub trace_chain: Vec<teaql_core::TraceNode>,
343    pub payload: LogPayload,
344}
345
346#[derive(Debug, Clone, PartialEq)]
347pub enum LogPayload {
348    Sql(SqlLogEntry),
349    Info(InfoLogEntry),
350}
351
352#[derive(Debug, Clone, PartialEq)]
353pub struct InfoLogEntry {
354    pub message: String,
355}
356
357#[derive(Clone, Default)]
358pub struct UnifiedLogBuffer {
359    pub entries: std::sync::Arc<Mutex<Vec<UnifiedLogEntry>>>,
360}
361
362/// Context-owned proof required by the provider SPI. Its private field prevents
363/// application crates from invoking a schema provider directly.
364///
365/// ```compile_fail
366/// let _ = teaql_runtime::SchemaInvocation { _context_owned: () };
367/// ```
368pub struct SchemaInvocation {
369    _context_owned: (),
370}
371
372pub trait SchemaProvider: Send + Sync {
373    fn ensure_schema<'a>(
374        &'a self,
375        context: &'a UserContext,
376        invocation: &'a SchemaInvocation,
377    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>>;
378}
379
380pub type GeneratedSchemaBootstrapFuture<'a> =
381    Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>>;
382pub type GeneratedSchemaBootstrap =
383    for<'a> fn(&'a UserContext) -> GeneratedSchemaBootstrapFuture<'a>;
384
385#[derive(Clone, Debug, PartialEq, Eq)]
386pub enum FixEvidenceSource {
387    Clock,
388    Context,
389}
390
391#[derive(Clone, Debug, PartialEq, Eq)]
392pub struct FixEvidence {
393    pub entity_type: String,
394    pub model_path: String,
395    pub source: FixEvidenceSource,
396    pub source_label: String,
397}
398
399impl FixEvidence {
400    pub fn new(
401        entity_type: &str,
402        model_path: &str,
403        source: FixEvidenceSource,
404        source_label: &str,
405    ) -> Self {
406        assert!(
407            !entity_type.trim().is_empty(),
408            "entity_type must not be blank"
409        );
410        assert!(
411            !model_path.trim().is_empty(),
412            "model_path must not be blank"
413        );
414        assert!(
415            !source_label.trim().is_empty(),
416            "source_label must not be blank"
417        );
418        let normalized = source_label.to_ascii_lowercase();
419        assert!(
420            !normalized.contains("authorization")
421                && !normalized.contains("cookie")
422                && !normalized.contains("token="),
423            "source_label must be a safe framework label"
424        );
425        Self {
426            entity_type: entity_type.to_owned(),
427            model_path: model_path.to_owned(),
428            source,
429            source_label: source_label.to_owned(),
430        }
431    }
432}
433
434pub struct UserContext {
435    active_root: OnceLock<ContextEntityRef>,
436    pub(crate) metadata: Option<Box<dyn MetadataStore>>,
437    pub(crate) entity_registry: Option<Box<dyn EntityRegistry>>,
438    pub(crate) entity_graph_decoders: InMemoryEntityGraphDecoderRegistry,
439    pub(crate) entity_data_service_behavior_registry:
440        Option<Box<dyn EntityDataServiceBehaviorRegistry>>,
441    pub(crate) request_policy: Option<Box<dyn RequestPolicy>>,
442    pub(crate) checker_registry: Option<Box<dyn CheckerRegistry>>,
443    pub(crate) event_sink: Option<Box<dyn RawAuditEventSink>>,
444    pub(crate) custom_event_sink: Option<Box<dyn crate::SafeAuditEventSink>>,
445    pub(crate) internal_id_generator: Option<Box<dyn InternalIdGenerator>>,
446    schema_provider: Option<Box<dyn SchemaProvider>>,
447    generated_schema_bootstraps: Vec<GeneratedSchemaBootstrap>,
448    language: Language,
449    i18n_catalog: Arc<crate::I18nCatalog>,
450    typed_resources: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
451    named_resources: BTreeMap<String, Box<dyn Any + Send + Sync>>,
452    locals: BTreeMap<String, Value>,
453    pub(crate) initial_graphs: Vec<GraphNode>,
454    pub(crate) root_graphs: Vec<GraphNode>,
455    entity_runtime_state: EntityRuntimeState,
456    sql_log_options: SqlLogOptions,
457    sql_log_entries: Mutex<Vec<SqlLogEntry>>,
458    user_identifier: Option<String>,
459    timezone: Option<String>,
460    trace_id: String,
461    continuous_page_cursor_store: std::sync::Arc<dyn ContinuousPageCursorStore>,
462    continuous_page_observation: Mutex<(String, Option<String>)>,
463    id_set_store: Arc<dyn IdSetStore>,
464    id_set_observation: Mutex<(String, Option<u64>)>,
465    local_lock_owner: u64,
466    remote_lock_owner: String,
467    runtime_telemetry: Arc<dyn crate::RuntimeTelemetry>,
468    last_fix_evidence: Mutex<Vec<FixEvidence>>,
469}
470
471#[derive(Clone, Copy)]
472struct LocalLockEntry {
473    owner: u64,
474    expires_at: Option<Instant>,
475}
476
477#[derive(Default)]
478struct ProcessLocalLocks {
479    entries: Mutex<HashMap<String, LocalLockEntry>>,
480    changed: Condvar,
481}
482
483static PROCESS_LOCAL_LOCKS: OnceLock<ProcessLocalLocks> = OnceLock::new();
484static NEXT_LOCAL_LOCK_OWNER: AtomicU64 = AtomicU64::new(1);
485
486impl Default for UserContext {
487    fn default() -> Self {
488        let pid = std::process::id();
489        let thread_id_str = format!("{:?}", std::thread::current().id());
490        let numeric_thread_id = thread_id_str
491            .strip_prefix("ThreadId(")
492            .and_then(|s| s.strip_suffix(")"))
493            .unwrap_or(&thread_id_str);
494        let os_user = std::env::var("USER")
495            .or_else(|_| std::env::var("USERNAME"))
496            .unwrap_or_else(|_| "main".to_owned());
497        let user_id = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
498        let owner_sequence = NEXT_LOCAL_LOCK_OWNER.fetch_add(1, Ordering::Relaxed);
499        Self {
500            active_root: OnceLock::new(),
501            metadata: None,
502            entity_registry: None,
503            entity_graph_decoders: InMemoryEntityGraphDecoderRegistry::default(),
504            entity_data_service_behavior_registry: None,
505            request_policy: None,
506            checker_registry: None,
507            event_sink: None,
508            custom_event_sink: None,
509            internal_id_generator: None,
510            schema_provider: None,
511            language: Language::default(),
512            i18n_catalog: crate::I18nCatalog::builtin().clone(),
513            typed_resources: HashMap::new(),
514            named_resources: BTreeMap::new(),
515            locals: BTreeMap::new(),
516            initial_graphs: Vec::new(),
517            root_graphs: Vec::new(),
518            generated_schema_bootstraps: Vec::new(),
519            entity_runtime_state: EntityRuntimeState::default(),
520            // Query and Mutation diagnostic SQL logging are on by default.
521            // They remain independently controllable because performance and
522            // retention policy may differ between the two operation families.
523            sql_log_options: SqlLogOptions::default(),
524            sql_log_entries: Mutex::new(Vec::new()),
525            user_identifier: Some(user_id),
526            timezone: Some("UTC".to_owned()),
527            trace_id: format!(
528                "req-{pid}-{numeric_thread_id}-{:x}",
529                std::time::SystemTime::now()
530                    .duration_since(std::time::UNIX_EPOCH)
531                    .unwrap_or_default()
532                    .as_micros()
533            ),
534            continuous_page_cursor_store: std::sync::Arc::new(
535                InMemoryContinuousPageCursorStore::default(),
536            ),
537            continuous_page_observation: Mutex::new(("DISABLED".to_owned(), None)),
538            id_set_store: Arc::new(InMemoryIdSetStore::default()),
539            id_set_observation: Mutex::new(("ID_SET_DISABLED".to_owned(), None)),
540            local_lock_owner: owner_sequence,
541            remote_lock_owner: format!(
542                "teaql:{pid}:{owner_sequence}:{}",
543                SystemTime::now()
544                    .duration_since(SystemTime::UNIX_EPOCH)
545                    .unwrap_or_default()
546                    .as_nanos()
547            ),
548            runtime_telemetry: Arc::new(crate::NoopRuntimeTelemetry),
549            last_fix_evidence: Mutex::new(Vec::new()),
550        }
551    }
552}
553
554#[async_trait::async_trait]
555pub trait DataStore: Send + Sync + 'static {
556    async fn get(&self, key: &str) -> Option<Value>;
557    async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>);
558    async fn remove(&self, key: &str);
559}
560
561/// Provider-neutral distributed lock boundary.
562///
563/// Implementations must associate an acquired lock with `owner_token` and
564/// release it only while that token still owns the key. A zero timeout is one
565/// non-blocking attempt; a zero expiry means no automatic lease expiry.
566#[async_trait::async_trait]
567pub trait RemoteLockProvider: Send + Sync + 'static {
568    async fn try_remote_lock(
569        &self,
570        key: &str,
571        owner_token: &str,
572        timeout_millis: u64,
573        expire_millis: u64,
574    ) -> bool;
575
576    async fn unlock_remote(&self, key: &str, owner_token: &str) -> bool;
577}
578
579#[derive(Default)]
580pub struct InMemoryDataStore {
581    cache: std::sync::RwLock<HashMap<String, (Value, Option<std::time::Instant>)>>,
582}
583
584#[async_trait::async_trait]
585impl DataStore for InMemoryDataStore {
586    async fn get(&self, key: &str) -> Option<Value> {
587        let lock = self.cache.read().unwrap();
588        if let Some((val, expires_at)) = lock.get(key) {
589            if let Some(exp) = expires_at {
590                if std::time::Instant::now() > *exp {
591                    return None;
592                }
593            }
594            return Some(val.clone());
595        }
596        None
597    }
598
599    async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>) {
600        let mut lock = self.cache.write().unwrap();
601        let expires_at = timeout_seconds
602            .map(|secs| std::time::Instant::now() + std::time::Duration::from_secs(secs));
603        lock.insert(key.to_string(), (value, expires_at));
604    }
605
606    async fn remove(&self, key: &str) {
607        let mut lock = self.cache.write().unwrap();
608        lock.remove(key);
609    }
610}
611
612impl UserContext {
613    pub fn new() -> Self {
614        Self::default()
615    }
616
617    pub fn with_active_root(mut self, entity_type: impl Into<String>, id: u64) -> Self {
618        let entity_type = crate::canonical_id_space_entity(&entity_type.into());
619        assert!(
620            !entity_type.trim().is_empty(),
621            "active root entity type is required"
622        );
623        assert!(id > 0, "active root id must be positive");
624        self.active_root
625            .set(ContextEntityRef { entity_type, id })
626            .expect("active root may only be assigned once");
627        self
628    }
629
630    #[doc(hidden)]
631    pub fn set_generated_bootstrap_active_root(
632        &self,
633        entity_type: impl Into<String>,
634        id: u64,
635    ) -> Result<(), RuntimeError> {
636        let entity_type = crate::canonical_id_space_entity(&entity_type.into());
637        if entity_type.trim().is_empty() || id == 0 {
638            return Err(RuntimeError::Schema(
639                "invalid generated active root".to_owned(),
640            ));
641        }
642        match self.active_root.get() {
643            Some(existing) if existing.entity_type == entity_type && existing.id == id => Ok(()),
644            Some(existing) => Err(RuntimeError::Schema(format!(
645                "active root already set to {}:{}",
646                existing.entity_type, existing.id
647            ))),
648            None => self
649                .active_root
650                .set(ContextEntityRef { entity_type, id })
651                .map_err(|_| RuntimeError::Schema("active root initialization raced".to_owned())),
652        }
653    }
654
655    pub fn require_active_root(
656        &self,
657        expected_entity_type: &str,
658    ) -> Result<&ContextEntityRef, ContextRootError> {
659        let canonical_expected = crate::canonical_id_space_entity(expected_entity_type);
660        match self.active_root.get() {
661            Some(root) if root.entity_type == canonical_expected => Ok(root),
662            actual_root => Err(ContextRootError {
663                expected_entity_type: canonical_expected,
664                actual_root: actual_root.cloned(),
665            }),
666        }
667    }
668
669    pub(crate) fn active_root_ref(&self) -> Option<&ContextEntityRef> {
670        self.active_root.get()
671    }
672
673    pub fn with_runtime_telemetry(mut self, telemetry: Arc<dyn crate::RuntimeTelemetry>) -> Self {
674        self.runtime_telemetry = telemetry;
675        self
676    }
677
678    pub fn set_runtime_telemetry(&mut self, telemetry: Arc<dyn crate::RuntimeTelemetry>) {
679        self.runtime_telemetry = telemetry;
680    }
681
682    pub fn runtime_telemetry(&self) -> &Arc<dyn crate::RuntimeTelemetry> {
683        &self.runtime_telemetry
684    }
685
686    pub(crate) fn runtime_telemetry_is_noop(&self) -> bool {
687        self.runtime_telemetry.is_noop()
688    }
689
690    pub fn start_runtime_operation(
691        &self,
692        operation: crate::RuntimeOperation,
693    ) -> crate::FailOpenRuntimeTelemetryScope {
694        crate::start_runtime_operation(&self.runtime_telemetry, operation)
695    }
696
697    pub fn try_local_lock(&self, key: &str, timeout_millis: u64, expire_millis: u64) -> bool {
698        let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
699        let deadline = Instant::now() + Duration::from_millis(timeout_millis);
700        let mut entries = locks.entries.lock().expect("local lock state poisoned");
701        loop {
702            let now = Instant::now();
703            match entries.get(key).copied() {
704                None => {
705                    entries.insert(
706                        key.to_owned(),
707                        LocalLockEntry {
708                            owner: self.local_lock_owner,
709                            expires_at: (expire_millis > 0)
710                                .then(|| now + Duration::from_millis(expire_millis)),
711                        },
712                    );
713                    return true;
714                }
715                Some(current)
716                    if current.owner == self.local_lock_owner
717                        || current.expires_at.is_some_and(|expiry| now >= expiry) =>
718                {
719                    entries.insert(
720                        key.to_owned(),
721                        LocalLockEntry {
722                            owner: self.local_lock_owner,
723                            expires_at: (expire_millis > 0)
724                                .then(|| now + Duration::from_millis(expire_millis)),
725                        },
726                    );
727                    return true;
728                }
729                Some(current) => {
730                    if timeout_millis == 0 || now >= deadline {
731                        return false;
732                    }
733                    let wake_after = current
734                        .expires_at
735                        .map(|expiry| expiry.saturating_duration_since(now))
736                        .unwrap_or_else(|| deadline.saturating_duration_since(now))
737                        .min(deadline.saturating_duration_since(now));
738                    let waited = locks
739                        .changed
740                        .wait_timeout(entries, wake_after)
741                        .expect("local lock state poisoned");
742                    entries = waited.0;
743                }
744            }
745        }
746    }
747
748    pub fn unlock_local(&self, key: &str) {
749        let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
750        let mut entries = locks.entries.lock().expect("local lock state poisoned");
751        if entries
752            .get(key)
753            .is_some_and(|entry| entry.owner == self.local_lock_owner)
754        {
755            entries.remove(key);
756            locks.changed.notify_all();
757        }
758    }
759
760    /// Attempts to acquire a provider-backed distributed lock.
761    ///
762    /// A missing provider remains a no-op success, matching the optional
763    /// Remote Lock boundary in the other TeaQL runtimes. Install an
764    /// `Arc<dyn RemoteLockProvider>` resource to enable distributed exclusion.
765    pub async fn try_remote_lock(
766        &self,
767        key: &str,
768        timeout_millis: u64,
769        expire_millis: u64,
770    ) -> bool {
771        match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
772            Some(provider) => {
773                provider
774                    .try_remote_lock(key, &self.remote_lock_owner, timeout_millis, expire_millis)
775                    .await
776            }
777            None => true,
778        }
779    }
780
781    /// Releases a distributed lock only when this context still owns it.
782    pub async fn unlock_remote(&self, key: &str) -> bool {
783        match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
784            Some(provider) => provider.unlock_remote(key, &self.remote_lock_owner).await,
785            None => true,
786        }
787    }
788
789    pub fn user_identifier(&self) -> Option<&str> {
790        self.user_identifier.as_deref()
791    }
792
793    pub fn set_user_identifier(&mut self, user_identifier: impl Into<String>) {
794        self.user_identifier = Some(user_identifier.into());
795    }
796
797    pub fn set_continuous_page_cursor_store(
798        &mut self,
799        store: std::sync::Arc<dyn ContinuousPageCursorStore>,
800    ) {
801        self.continuous_page_cursor_store = store;
802    }
803
804    pub fn continuous_page_plan(&self) -> Option<String> {
805        self.continuous_page_observation
806            .lock()
807            .ok()
808            .map(|value| value.0.clone())
809    }
810
811    pub fn continuous_page_cursor_id(&self) -> Option<String> {
812        self.continuous_page_observation
813            .lock()
814            .ok()
815            .and_then(|value| value.1.clone())
816    }
817
818    pub(crate) fn observe_continuous_page(
819        &self,
820        plan: impl Into<String>,
821        cursor_id: Option<String>,
822    ) {
823        if let Ok(mut observation) = self.continuous_page_observation.lock() {
824            *observation = (plan.into(), cursor_id);
825        }
826    }
827
828    pub(crate) fn continuous_page_cursor_store(&self) -> &dyn ContinuousPageCursorStore {
829        self.continuous_page_cursor_store.as_ref()
830    }
831
832    pub fn set_id_set_store(&mut self, store: Arc<dyn IdSetStore>) {
833        self.id_set_store = store;
834    }
835
836    pub fn id_set_plan(&self) -> Option<String> {
837        self.id_set_observation
838            .lock()
839            .ok()
840            .map(|observation| observation.0.clone())
841    }
842
843    pub fn id_set_count(&self) -> Option<u64> {
844        self.id_set_observation
845            .lock()
846            .ok()
847            .and_then(|observation| observation.1)
848    }
849
850    pub(crate) fn observe_id_set(&self, plan: impl Into<String>, count: Option<u64>) {
851        if let Ok(mut observation) = self.id_set_observation.lock() {
852            *observation = (plan.into(), count);
853        }
854    }
855
856    pub(crate) fn id_set_store(&self) -> &dyn IdSetStore {
857        self.id_set_store.as_ref()
858    }
859
860    pub(crate) fn id_set_build_lock(&self, query_key: &str) -> Arc<futures_util::lock::Mutex<()>> {
861        id_set_build_lock(query_key)
862    }
863
864    pub fn with_user_identifier(mut self, user_identifier: impl Into<String>) -> Self {
865        self.user_identifier = Some(user_identifier.into());
866        self
867    }
868
869    pub fn set_user_identifier_option(&mut self, user_identifier: Option<String>) {
870        self.user_identifier = user_identifier;
871    }
872
873    pub fn with_user_identifier_option(mut self, user_identifier: Option<String>) -> Self {
874        self.user_identifier = user_identifier;
875        self
876    }
877
878    pub fn timezone(&self) -> Option<&str> {
879        self.timezone.as_deref()
880    }
881
882    pub fn set_timezone(&mut self, timezone: impl Into<String>) {
883        self.timezone = Some(timezone.into());
884    }
885
886    pub fn with_timezone(mut self, timezone: impl Into<String>) -> Self {
887        self.timezone = Some(timezone.into());
888        self
889    }
890
891    pub fn trace_id(&self) -> &str {
892        &self.trace_id
893    }
894
895    pub fn set_trace_id(&mut self, trace_id: impl Into<String>) {
896        self.trace_id = trace_id.into();
897    }
898
899    pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
900        self.trace_id = trace_id.into();
901        self
902    }
903
904    pub fn with_module(mut self, module: crate::RuntimeModule) -> Self {
905        module.apply_to(&mut self);
906        self
907    }
908
909    pub fn entity_runtime_state(&self) -> EntityRuntimeState {
910        // UserContext owns only the immutable identity-graph anchor. Every query/new-entity
911        // operation receives fresh mutation state, even when the same context is reused.
912        EntityRuntimeState::fresh_with_shared_graph(&self.entity_runtime_state)
913    }
914
915    pub fn initial_graphs(&self) -> &[GraphNode] {
916        &self.initial_graphs
917    }
918
919    pub fn set_initial_graphs(&mut self, graphs: Vec<GraphNode>) {
920        self.initial_graphs = graphs;
921    }
922
923    pub fn root_graphs(&self) -> &[GraphNode] {
924        &self.root_graphs
925    }
926
927    pub fn set_root_graphs(&mut self, graphs: Vec<GraphNode>) {
928        self.root_graphs = graphs;
929    }
930
931    pub fn with_metadata(mut self, metadata: impl MetadataStore + 'static) -> Self {
932        self.metadata = Some(Box::new(metadata));
933        self
934    }
935
936    pub fn set_metadata(&mut self, metadata: impl MetadataStore + 'static) {
937        self.metadata = Some(Box::new(metadata));
938    }
939
940    pub fn with_entity_registry(mut self, registry: impl EntityRegistry + 'static) -> Self {
941        self.entity_registry = Some(Box::new(registry));
942        self
943    }
944
945    pub fn set_entity_registry(&mut self, registry: impl EntityRegistry + 'static) {
946        self.entity_registry = Some(Box::new(registry));
947    }
948
949    pub fn set_entity_graph_decoder_registry(
950        &mut self,
951        registry: InMemoryEntityGraphDecoderRegistry,
952    ) {
953        self.entity_graph_decoders = registry;
954    }
955
956    pub(crate) fn has_entity_graph_decoder(&self, entity: &str) -> bool {
957        self.entity_graph_decoders.contains(entity)
958    }
959
960    pub(crate) fn decode_compact_entity_into_graph(
961        &self,
962        entity: &str,
963        row: teaql_core::CompactRow,
964        root: &EntityRuntimeState,
965        graph: &mut EntityGraphBuilder,
966    ) -> Result<(), teaql_core::EntityError> {
967        self.entity_graph_decoders
968            .decode_compact(entity, row, root, graph)
969    }
970
971    pub(crate) fn decode_compact_entity_list_into_graph(
972        &self,
973        entity: &str,
974        rows: Vec<teaql_core::CompactRow>,
975        root: &EntityRuntimeState,
976        graph: &mut EntityGraphBuilder,
977        owner_entity: &str,
978        owner_id: u64,
979        relation: &str,
980    ) -> Result<(), teaql_core::EntityError> {
981        self.entity_graph_decoders.decode_compact_list(
982            entity,
983            rows,
984            root,
985            graph,
986            owner_entity,
987            owner_id,
988            relation,
989        )
990    }
991
992    pub(crate) fn decode_compact_entity_batch_into_graph(
993        &self,
994        entity: &str,
995        rows: Vec<teaql_core::CompactRow>,
996        root: &EntityRuntimeState,
997        graph: &mut EntityGraphBuilder,
998    ) -> Result<(), teaql_core::EntityError> {
999        self.entity_graph_decoders
1000            .decode_compact_batch(entity, rows, root, graph)
1001    }
1002
1003    pub(crate) fn decode_compact_entity_option_into_graph(
1004        &self,
1005        entity: &str,
1006        rows: Vec<teaql_core::CompactRow>,
1007        root: &EntityRuntimeState,
1008        graph: &mut EntityGraphBuilder,
1009        owner_entity: &str,
1010        owner_id: u64,
1011        relation: &str,
1012    ) -> Result<(), teaql_core::EntityError> {
1013        self.entity_graph_decoders.decode_compact_option(
1014            entity,
1015            rows,
1016            root,
1017            graph,
1018            owner_entity,
1019            owner_id,
1020            relation,
1021        )
1022    }
1023
1024    pub fn with_entity_data_service_behavior_registry(
1025        mut self,
1026        registry: impl EntityDataServiceBehaviorRegistry + 'static,
1027    ) -> Self {
1028        self.entity_data_service_behavior_registry = Some(Box::new(registry));
1029        self
1030    }
1031
1032    pub fn set_entity_data_service_behavior_registry(
1033        &mut self,
1034        registry: impl EntityDataServiceBehaviorRegistry + 'static,
1035    ) {
1036        self.entity_data_service_behavior_registry = Some(Box::new(registry));
1037    }
1038
1039    pub fn with_request_policy(mut self, policy: impl RequestPolicy + 'static) -> Self {
1040        self.request_policy = Some(Box::new(policy));
1041        self
1042    }
1043
1044    pub fn set_request_policy(&mut self, policy: impl RequestPolicy + 'static) {
1045        self.request_policy = Some(Box::new(policy));
1046    }
1047
1048    pub fn clear_request_policy(&mut self) {
1049        self.request_policy = None;
1050    }
1051
1052    pub fn with_checker_registry(mut self, registry: impl CheckerRegistry + 'static) -> Self {
1053        self.checker_registry = Some(Box::new(registry));
1054        self
1055    }
1056
1057    pub fn set_checker_registry(&mut self, registry: impl CheckerRegistry + 'static) {
1058        self.checker_registry = Some(Box::new(registry));
1059    }
1060
1061    pub(crate) fn with_event_sink(mut self, sink: impl RawAuditEventSink + 'static) -> Self {
1062        self.event_sink = Some(Box::new(sink));
1063        self
1064    }
1065
1066    pub(crate) fn set_event_sink(&mut self, sink: impl RawAuditEventSink + 'static) {
1067        self.event_sink = Some(Box::new(sink));
1068    }
1069
1070    pub fn with_custom_event_sink(
1071        mut self,
1072        sink: impl crate::SafeAuditEventSink + 'static,
1073    ) -> Self {
1074        self.custom_event_sink = Some(Box::new(sink));
1075        self
1076    }
1077
1078    pub fn set_custom_event_sink(&mut self, sink: impl crate::SafeAuditEventSink + 'static) {
1079        self.custom_event_sink = Some(Box::new(sink));
1080    }
1081
1082    pub fn with_internal_id_generator(
1083        mut self,
1084        generator: impl InternalIdGenerator + 'static,
1085    ) -> Self {
1086        self.internal_id_generator = Some(Box::new(generator));
1087        self
1088    }
1089
1090    pub fn set_internal_id_generator(&mut self, generator: impl InternalIdGenerator + 'static) {
1091        self.internal_id_generator = Some(Box::new(generator));
1092    }
1093
1094    pub fn with_schema_provider(mut self, provider: impl SchemaProvider + 'static) -> Self {
1095        self.schema_provider = Some(Box::new(provider));
1096        self
1097    }
1098
1099    pub fn set_schema_provider(&mut self, provider: impl SchemaProvider + 'static) {
1100        self.schema_provider = Some(Box::new(provider));
1101    }
1102
1103    pub async fn ensure_schema(&self) -> Result<(), RuntimeError> {
1104        let provider = self
1105            .schema_provider
1106            .as_ref()
1107            .ok_or_else(|| RuntimeError::Schema("missing schema provider".to_owned()))?;
1108        let invocation = SchemaInvocation { _context_owned: () };
1109        provider.ensure_schema(self, &invocation).await?;
1110        GENERATED_SCHEMA_BOOTSTRAP_MODE
1111            .scope((), async {
1112                for bootstrap in &self.generated_schema_bootstraps {
1113                    bootstrap(self).await?;
1114                }
1115                Ok::<(), RuntimeError>(())
1116            })
1117            .await?;
1118        Ok(())
1119    }
1120
1121    pub(crate) fn is_generated_schema_bootstrap(&self) -> bool {
1122        GENERATED_SCHEMA_BOOTSTRAP_MODE
1123            .try_with(|_| true)
1124            .unwrap_or(false)
1125    }
1126
1127    pub(crate) fn set_generated_schema_bootstraps(
1128        &mut self,
1129        bootstraps: Vec<GeneratedSchemaBootstrap>,
1130    ) {
1131        self.generated_schema_bootstraps = bootstraps;
1132    }
1133
1134    #[doc(hidden)]
1135    pub fn initialize_generated_bootstrap_entity<E: teaql_core::Entity>(
1136        &self,
1137        entity: &mut E,
1138        entity_name: &str,
1139        fixed_id: u64,
1140    ) -> Result<(), RuntimeError> {
1141        let generator = self.internal_id_generator.as_ref().ok_or_else(|| {
1142            RuntimeError::IdGeneration("missing internal ID generator".to_owned())
1143        })?;
1144        generator.ensure_floor(entity_name, fixed_id)?;
1145        entity.mark_as_new();
1146        Ok(())
1147    }
1148
1149    pub fn with_language(mut self, language: Language) -> Self {
1150        self.language = language;
1151        self
1152    }
1153
1154    pub fn set_language(&mut self, language: Language) {
1155        self.language = language;
1156    }
1157
1158    pub fn with_i18n_catalog(mut self, catalog: Arc<crate::I18nCatalog>) -> Self {
1159        self.i18n_catalog = catalog;
1160        self
1161    }
1162
1163    pub fn set_i18n_catalog(&mut self, catalog: Arc<crate::I18nCatalog>) {
1164        self.i18n_catalog = catalog;
1165    }
1166
1167    pub fn with_sql_log_options(mut self, options: SqlLogOptions) -> Self {
1168        self.sql_log_options = options;
1169        self
1170    }
1171
1172    pub fn set_sql_log_options(&mut self, options: SqlLogOptions) {
1173        self.sql_log_options = options;
1174    }
1175
1176    pub fn enable_select_sql_log(&mut self) {
1177        self.sql_log_options.select = true;
1178    }
1179
1180    pub fn enable_mutation_sql_log(&mut self) {
1181        self.sql_log_options.mutation = true;
1182    }
1183
1184    pub fn disable_select_sql_log(&mut self) {
1185        self.sql_log_options.select = false;
1186    }
1187
1188    pub fn disable_mutation_sql_log(&mut self) {
1189        self.sql_log_options.mutation = false;
1190    }
1191
1192    pub fn enable_all_sql_log(&mut self) {
1193        self.sql_log_options = SqlLogOptions::all();
1194    }
1195
1196    pub fn disable_sql_log(&mut self) {
1197        self.sql_log_options = SqlLogOptions::disabled();
1198        self.clear_sql_logs();
1199    }
1200
1201    pub fn sql_log_options(&self) -> SqlLogOptions {
1202        self.sql_log_options
1203    }
1204
1205    pub fn sql_logs(&self) -> Vec<SqlLogEntry> {
1206        self.sql_log_entries
1207            .lock()
1208            .map(|entries| entries.clone())
1209            .unwrap_or_default()
1210    }
1211
1212    pub fn clear_sql_logs(&self) {
1213        if let Ok(mut entries) = self.sql_log_entries.lock() {
1214            entries.clear();
1215        }
1216    }
1217
1218    pub(crate) fn record_sql_log(
1219        &self,
1220        operation: SqlLogOperation,
1221        query: &CompiledQuery,
1222        database_kind: DatabaseKind,
1223        started_at: SystemTime,
1224        ended_at: SystemTime,
1225        elapsed: Duration,
1226        result_count: Option<usize>,
1227        result_type: Option<String>,
1228        affected_rows: Option<u64>,
1229        trace_chain: Vec<teaql_core::TraceNode>,
1230    ) {
1231        if !self.sql_log_options.enabled_for(operation) {
1232            return;
1233        }
1234        let debug_sql = query.debug_sql(database_kind);
1235        let result_summary = sql_result_summary(
1236            operation,
1237            result_count,
1238            result_type.as_deref(),
1239            affected_rows,
1240            &debug_sql,
1241        );
1242
1243        let trace_path = canonical_sql_trace_path(
1244            operation,
1245            &format!("{database_kind:?}").to_ascii_lowercase(),
1246            &trace_chain,
1247        );
1248        let sql_log_entry = SqlLogEntry {
1249            operation,
1250            comment: trace_value(&trace_chain, teaql_core::TraceKind::Comment),
1251            purpose: trace_value(&trace_chain, teaql_core::TraceKind::Purpose),
1252            audit_reason: trace_value(&trace_chain, teaql_core::TraceKind::AuditReason),
1253            trace_path: trace_path.clone(),
1254            sql: query.sql.clone(),
1255            params: query.params.clone(),
1256            pretty_sql: pretty_sql(&debug_sql),
1257            debug_sql: debug_sql.clone(),
1258            started_at,
1259            ended_at,
1260            elapsed,
1261            result_summary: result_summary.clone(),
1262            result_count,
1263            result_type,
1264            affected_rows,
1265        };
1266
1267        if let Ok(mut entries) = self.sql_log_entries.lock() {
1268            entries.push(sql_log_entry.clone());
1269        }
1270
1271        if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
1272            if let Ok(mut entries) = buf.entries.lock() {
1273                entries.push(UnifiedLogEntry {
1274                    timestamp: started_at,
1275                    user_identifier: self.user_identifier.clone(),
1276                    trace_chain: trace_path.clone(),
1277                    payload: LogPayload::Sql(sql_log_entry.clone()),
1278                });
1279            }
1280        }
1281
1282        crate::log_formatter::LogManager::write_sql_log(&trace_path, &sql_log_entry);
1283    }
1284
1285    pub(crate) fn record_metadata_log(&self, metadata: &teaql_data_service::ExecutionMetadata) {
1286        let operation = match metadata.operation {
1287            teaql_data_service::DataServiceOperation::Query => SqlLogOperation::Select,
1288            teaql_data_service::DataServiceOperation::Insert => SqlLogOperation::Insert,
1289            teaql_data_service::DataServiceOperation::Update => SqlLogOperation::Update,
1290            teaql_data_service::DataServiceOperation::Delete => SqlLogOperation::Delete,
1291            teaql_data_service::DataServiceOperation::Recover => SqlLogOperation::Update,
1292            teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
1293            teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
1294        };
1295        if !self.sql_log_options.enabled_for(operation) {
1296            return;
1297        }
1298        if let Some(debug_sql) = &metadata.debug_query {
1299            let trace_path =
1300                canonical_sql_trace_path(operation, &metadata.backend, &metadata.trace_chain);
1301            let sql_log_entry = SqlLogEntry {
1302                operation,
1303                comment: trace_value(&metadata.trace_chain, teaql_core::TraceKind::Comment)
1304                    .or_else(|| metadata.comment.clone()),
1305                purpose: trace_value(&metadata.trace_chain, teaql_core::TraceKind::Purpose),
1306                audit_reason: trace_value(
1307                    &metadata.trace_chain,
1308                    teaql_core::TraceKind::AuditReason,
1309                ),
1310                trace_path: trace_path.clone(),
1311                sql: metadata.parameterized_query.clone().unwrap_or_default(),
1312                params: metadata.params.clone(),
1313                pretty_sql: pretty_sql(debug_sql),
1314                debug_sql: debug_sql.clone(),
1315                started_at: metadata.started_at,
1316                ended_at: metadata.ended_at,
1317                elapsed: metadata
1318                    .ended_at
1319                    .duration_since(metadata.started_at)
1320                    .unwrap_or_default(),
1321                result_count: metadata.result_count,
1322                result_type: None, // Not directly available
1323                affected_rows: metadata.affected_rows,
1324                result_summary: String::new(), // We can synthesize this if needed, or leave it empty/basic
1325            };
1326
1327            // synthesize a summary for the log
1328            let mut summary = String::new();
1329            if let Some(c) = metadata.result_count {
1330                summary = format!("{} rows returned", c);
1331            } else if let Some(a) = metadata.affected_rows {
1332                summary = format!("{} rows affected", a);
1333            }
1334
1335            let mut final_entry = sql_log_entry;
1336            final_entry.result_summary = summary;
1337
1338            if let Ok(mut entries) = self.sql_log_entries.lock() {
1339                entries.push(final_entry.clone());
1340            }
1341
1342            if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
1343                if let Ok(mut entries) = buf.entries.lock() {
1344                    entries.push(UnifiedLogEntry {
1345                        timestamp: metadata.started_at,
1346                        user_identifier: self.user_identifier.clone(),
1347                        trace_chain: trace_path.clone(),
1348                        payload: LogPayload::Sql(final_entry.clone()),
1349                    });
1350                }
1351            }
1352
1353            crate::log_formatter::LogManager::write_sql_log(&trace_path, &final_entry);
1354        }
1355    }
1356
1357    pub fn language(&self) -> Language {
1358        self.language
1359    }
1360
1361    pub fn set_language_code(&mut self, code: &str) -> Result<(), RuntimeError> {
1362        let Some(language) = Language::from_code(code) else {
1363            return Err(RuntimeError::UnsupportedLocale(code.to_owned()));
1364        };
1365        self.language = language;
1366        Ok(())
1367    }
1368
1369    pub fn set_locale_code(&mut self, code: &str) -> Result<(), RuntimeError> {
1370        self.set_language_code(code)
1371    }
1372
1373    pub fn generate_id(&self, entity: &str) -> Result<Option<u64>, RuntimeError> {
1374        self.internal_id_generator
1375            .as_ref()
1376            .map(|generator| generator.generate_id(entity))
1377            .transpose()
1378    }
1379
1380    pub fn next_id(&self, entity: &str) -> Result<u64, RuntimeError> {
1381        match self.generate_id(entity)? {
1382            Some(id) => Ok(id),
1383            None => local_id_generator().generate_id(entity),
1384        }
1385    }
1386
1387    pub fn entity(&self, name: &str) -> Option<&EntityDescriptor> {
1388        self.metadata
1389            .as_ref()
1390            .and_then(|metadata| metadata.entity(name))
1391    }
1392
1393    pub fn all_entities(&self) -> Vec<&EntityDescriptor> {
1394        self.metadata
1395            .as_ref()
1396            .map(|metadata| metadata.all_entities())
1397            .unwrap_or_default()
1398    }
1399
1400    pub fn require_entity(&self, name: &str) -> Result<&EntityDescriptor, RuntimeError> {
1401        self.entity(name)
1402            .ok_or_else(|| RuntimeError::MissingEntity(name.to_owned()))
1403    }
1404
1405    pub fn insert_resource<T>(&mut self, resource: T)
1406    where
1407        T: Send + Sync + 'static,
1408    {
1409        self.typed_resources
1410            .insert(TypeId::of::<T>(), Box::new(resource));
1411    }
1412
1413    pub fn get_resource<T>(&self) -> Option<&T>
1414    where
1415        T: Send + Sync + 'static,
1416    {
1417        self.typed_resources
1418            .get(&TypeId::of::<T>())
1419            .and_then(|value| value.downcast_ref::<T>())
1420    }
1421
1422    pub fn require_resource<T>(&self) -> Result<&T, ContextError>
1423    where
1424        T: Send + Sync + 'static,
1425    {
1426        self.get_resource::<T>()
1427            .ok_or(ContextError::MissingTypedResource(
1428                std::any::type_name::<T>(),
1429            ))
1430    }
1431
1432    pub fn insert_named_resource<T>(&mut self, name: impl Into<String>, resource: T)
1433    where
1434        T: Send + Sync + 'static,
1435    {
1436        self.named_resources.insert(name.into(), Box::new(resource));
1437    }
1438
1439    pub fn get_named_resource<T>(&self, name: &str) -> Option<&T>
1440    where
1441        T: Send + Sync + 'static,
1442    {
1443        self.named_resources
1444            .get(name)
1445            .and_then(|value| value.downcast_ref::<T>())
1446    }
1447
1448    pub fn require_named_resource<T>(&self, name: &str) -> Result<&T, ContextError>
1449    where
1450        T: Send + Sync + 'static,
1451    {
1452        self.get_named_resource::<T>(name)
1453            .ok_or_else(|| ContextError::MissingResource(name.to_owned()))
1454    }
1455
1456    pub fn put_local(&mut self, key: impl Into<String>, value: impl Into<Value>) {
1457        self.locals.insert(key.into(), value.into());
1458    }
1459
1460    pub fn local(&self, key: &str) -> Option<&Value> {
1461        self.locals.get(key)
1462    }
1463
1464    pub fn remove_local(&mut self, key: &str) -> Option<Value> {
1465        self.locals.remove(key)
1466    }
1467
1468    pub fn has_entity_data_service(&self, entity: &str) -> bool {
1469        let in_registry = self
1470            .entity_registry
1471            .as_ref()
1472            .map(|registry| registry.contains(entity))
1473            .unwrap_or(false);
1474        in_registry || self.entity(entity).is_some()
1475    }
1476
1477    pub fn entity_data_service_behavior(
1478        &self,
1479        entity: &str,
1480    ) -> Option<std::sync::Arc<dyn EntityDataServiceBehavior>> {
1481        self.entity_data_service_behavior_registry
1482            .as_ref()
1483            .and_then(|registry| registry.behavior(entity))
1484    }
1485
1486    pub fn has_checker(&self, entity: &str) -> bool {
1487        self.checker_registry
1488            .as_ref()
1489            .and_then(|registry| registry.checker(entity))
1490            .is_some()
1491    }
1492
1493    /// One deterministic clock value for every Fix executed by the current
1494    /// graph save. The task-local scope keeps concurrent saves on one context
1495    /// isolated; standalone checker calls receive their own current value.
1496    pub fn fix_time(&self) -> teaql_core::time::Timestamp {
1497        crate::entity_save::current_graph_fix_time()
1498    }
1499
1500    pub fn record_fix_evidence(&self, evidence: FixEvidence) {
1501        crate::entity_save::record_graph_fix_evidence(evidence);
1502    }
1503
1504    pub(crate) fn replace_last_fix_evidence(&self, evidence: Vec<FixEvidence>) {
1505        *self.last_fix_evidence.lock().unwrap() = evidence;
1506    }
1507
1508    pub fn last_fix_evidence(&self) -> Vec<FixEvidence> {
1509        self.last_fix_evidence.lock().unwrap().clone()
1510    }
1511
1512    pub fn check_and_fix_values(
1513        &self,
1514        entity: &str,
1515        values: &mut crate::EntityValues,
1516    ) -> Result<(), RuntimeError> {
1517        self.check_and_fix_values_at(entity, values, &ObjectLocation::root())
1518    }
1519
1520    pub fn check_and_fix_values_at(
1521        &self,
1522        entity: &str,
1523        values: &mut crate::EntityValues,
1524        location: &ObjectLocation,
1525    ) -> Result<(), RuntimeError> {
1526        let status = CheckObjectStatus::from_values(values);
1527        let checker = self
1528            .checker_registry
1529            .as_ref()
1530            .and_then(|registry| registry.checker(entity));
1531        let mut results = CheckResults::new();
1532        if let Some(checker) = checker {
1533            checker.check_and_fix(self, values, location, &mut results);
1534        }
1535
1536        // Keep runtime validation aligned with the schema generated from the
1537        // same metadata. Custom checkers get the first chance to supply or fix
1538        // a value; afterwards every NOT NULL property must be present on a
1539        // create, and an update must not explicitly clear one.
1540        if let Some(descriptor) = self
1541            .metadata
1542            .as_ref()
1543            .and_then(|metadata| metadata.entity(entity))
1544        {
1545            for property in descriptor
1546                .properties
1547                .iter()
1548                // The optimistic-lock version is runtime-managed. Insert
1549                // preparation assigns its initial value after check/fix, so a
1550                // create caller must never be required to provide it.
1551                .filter(|property| !property.nullable && !property.is_version)
1552            {
1553                let missing = !values.contains_key(&property.name);
1554                let null = matches!(values.get(&property.name), Some(Value::Null));
1555                let property_location = location.clone().member(&property.name);
1556                let already_reported = results.iter().any(|result| {
1557                    result.rule == crate::CheckRule::Required
1558                        && result.location == property_location
1559                });
1560                if ((status.is_create() && missing) || null) && !already_reported {
1561                    results.push(CheckResult::required(property_location));
1562                }
1563            }
1564        }
1565        if results.is_empty() {
1566            return Ok(());
1567        }
1568        self.translate_check_results(&mut results);
1569        Err(RuntimeError::Check(results))
1570    }
1571
1572    pub fn translate_check_results(&self, results: &mut CheckResults) {
1573        for result in results {
1574            if result.message.is_none() {
1575                result.message = Some(
1576                    self.i18n_catalog
1577                        .translate_check_result(self.language, result),
1578                );
1579            }
1580        }
1581    }
1582
1583    pub fn send_event(&self, mut event: RawAuditEvent) -> Result<(), RuntimeError> {
1584        if self.is_generated_schema_bootstrap()
1585            && matches!(
1586                event.kind,
1587                crate::RawAuditEventKind::Created | crate::RawAuditEventKind::Updated
1588            )
1589        {
1590            let reason = event
1591                .trace_chain
1592                .last()
1593                .map(|node| node.comment.clone())
1594                .unwrap_or_else(|| "generated runtime bootstrap".to_owned());
1595            let resulting_version = event
1596                .new_values
1597                .as_ref()
1598                .and_then(|values| values.get("version"))
1599                .or_else(|| event.values.get("version"))
1600                .and_then(teaql_core::Value::try_i64);
1601            let occurred_at_millis = std::time::SystemTime::now()
1602                .duration_since(std::time::UNIX_EPOCH)
1603                .unwrap_or_default()
1604                .as_millis() as u64;
1605            event.bootstrap_audit = Some(crate::BootstrapAuditIdentity {
1606                actor: "teaql-generated-bootstrap".to_owned(),
1607                category: "runtime-bootstrap".to_owned(),
1608                reason,
1609                resulting_version,
1610                occurred_at_millis,
1611            });
1612        }
1613        let scope = self.start_runtime_operation(
1614            crate::RuntimeOperation::new("audit", format!("{}.event", event.entity))
1615                .attribute("teaql.entity.type", event.entity.clone()),
1616        );
1617        let result = self.send_event_inner(event);
1618        match &result {
1619            Ok(()) => scope.success(std::collections::BTreeMap::new()),
1620            Err(_) => scope.failure("audit_error"),
1621        }
1622        result
1623    }
1624
1625    fn send_event_inner(&self, event: RawAuditEvent) -> Result<(), RuntimeError> {
1626        if let Some(sink) = self.event_sink.as_ref() {
1627            sink.on_event(self, &event)?;
1628        }
1629        if let Some(sink) = self.custom_event_sink.as_ref() {
1630            let (mask_fields, max_len) = self
1631                .metadata
1632                .as_ref()
1633                .and_then(|metadata| metadata.entity(&event.entity))
1634                .map(|desc| (desc.audit_mask_fields.clone(), desc.audit_value_max_len))
1635                .unwrap_or_else(|| (vec![], None));
1636
1637            let safe_event = event.build_safe_event(&mask_fields, max_len);
1638            sink.on_safe_event(self, &safe_event)?;
1639        }
1640
1641        crate::log_formatter::LogManager::write_audit_log(&event);
1642
1643        Ok(())
1644    }
1645
1646    pub async fn get_in_store(&self, key: &str) -> Option<Value> {
1647        let store = self.get_resource::<Box<dyn DataStore>>()?;
1648        store.get(key).await
1649    }
1650
1651    pub async fn put_in_store(
1652        &self,
1653        key: &str,
1654        value: impl Into<Value>,
1655        timeout_seconds: Option<u64>,
1656    ) {
1657        if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1658            store.put(key, value.into(), timeout_seconds).await;
1659        }
1660    }
1661
1662    pub async fn clear_in_store(&self, key: &str) {
1663        if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1664            store.remove(key).await;
1665        }
1666    }
1667}
1668
1669fn extract_id_from_sql(sql: &str) -> Option<String> {
1670    let sql_lower = sql.to_lowercase();
1671    let where_idx = sql_lower.find("where")?;
1672    let where_clause = &sql_lower[where_idx + 5..];
1673
1674    let bytes = where_clause.as_bytes();
1675    let mut i = 0;
1676    while i < bytes.len() {
1677        if i + 1 < bytes.len() && &bytes[i..i + 2] == b"id" {
1678            // Check boundary before
1679            let prev_ok = i == 0 || {
1680                let prev_char = bytes[i - 1] as char;
1681                !prev_char.is_ascii_alphanumeric() && prev_char != '_' && prev_char != '.'
1682            };
1683            // Check boundary after
1684            let next_ok = i + 2 == bytes.len() || {
1685                let next_char = bytes[i + 2] as char;
1686                !next_char.is_ascii_alphanumeric() && next_char != '_'
1687            };
1688
1689            if prev_ok && next_ok {
1690                // Found the standalone "id" word!
1691                // Now look for "=" after it
1692                let mut j = i + 2;
1693                while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1694                    j += 1;
1695                }
1696                if j < bytes.len() && bytes[j] == b'=' {
1697                    j += 1;
1698                    while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1699                        j += 1;
1700                    }
1701                    // Now extract the value
1702                    let mut val_str = String::new();
1703                    if j < bytes.len() && bytes[j] == b'\'' {
1704                        j += 1; // consume single quote
1705                        while j < bytes.len() && bytes[j] != b'\'' {
1706                            val_str.push(bytes[j] as char);
1707                            j += 1;
1708                        }
1709                        return Some(val_str);
1710                    }
1711                    // No else needed — falls through to unquoted parsing
1712                    while j < bytes.len() {
1713                        let c = bytes[j] as char;
1714                        if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
1715                            break;
1716                        }
1717                        val_str.push(c);
1718                        j += 1;
1719                    }
1720                    if !val_str.is_empty() {
1721                        return Some(val_str);
1722                    }
1723                }
1724            }
1725        }
1726        i += 1;
1727    }
1728    None
1729}
1730
1731fn sql_result_summary(
1732    operation: SqlLogOperation,
1733    result_count: Option<usize>,
1734    result_type: Option<&str>,
1735    affected_rows: Option<u64>,
1736    debug_sql: &str,
1737) -> String {
1738    match operation {
1739        SqlLogOperation::Select => {
1740            let count = result_count.unwrap_or(0);
1741            match count {
1742                0 => "MISS".to_owned(),
1743                1 => match result_type {
1744                    Some(result_type) => extract_id_from_sql(debug_sql)
1745                        .map(|id| format!("{result_type}({id})"))
1746                        .unwrap_or_else(|| result_type.to_owned()),
1747                    None => "row".to_owned(),
1748                },
1749                _ => match result_type {
1750                    Some(result_type) => format!("{count}*{result_type}"),
1751                    None => format!("{count}*rows"),
1752                },
1753            }
1754        }
1755        _ => {
1756            let affected = affected_rows.unwrap_or(0);
1757            format!("{affected} UPDATED")
1758        }
1759    }
1760}
1761
1762fn trace_value(
1763    trace_path: &[teaql_core::TraceNode],
1764    kind: teaql_core::TraceKind,
1765) -> Option<String> {
1766    trace_path
1767        .iter()
1768        .rev()
1769        .find(|node| node.kind == kind)
1770        .map(|node| node.comment.clone())
1771}
1772
1773fn canonical_sql_trace_path(
1774    operation: SqlLogOperation,
1775    backend: &str,
1776    source: &[teaql_core::TraceNode],
1777) -> Vec<teaql_core::TraceNode> {
1778    use teaql_core::{TraceKind, TraceNode};
1779
1780    if source.iter().any(|node| node.kind == TraceKind::Operation)
1781        && source.iter().any(|node| node.kind == TraceKind::Provider)
1782        && source.iter().any(|node| node.kind == TraceKind::Sql)
1783    {
1784        return source
1785            .iter()
1786            .filter(|node| {
1787                !matches!(
1788                    node.kind,
1789                    TraceKind::Comment | TraceKind::Purpose | TraceKind::AuditReason
1790                )
1791            })
1792            .cloned()
1793            .collect();
1794    }
1795
1796    let entity = source
1797        .iter()
1798        .find(|node| !node.entity_type.trim().is_empty())
1799        .map(|node| node.entity_type.clone())
1800        .unwrap_or_else(|| "unknown".to_owned());
1801    let family = if operation.is_select() {
1802        "query"
1803    } else {
1804        "mutation"
1805    };
1806    let statement = match operation {
1807        SqlLogOperation::Select => "select",
1808        SqlLogOperation::Insert => "insert",
1809        SqlLogOperation::Update => "update",
1810        SqlLogOperation::Delete => "delete",
1811        SqlLogOperation::Recover => "recover",
1812    };
1813    let mut path = vec![TraceNode::typed(
1814        TraceKind::Operation,
1815        entity.clone(),
1816        None,
1817        family,
1818    )];
1819    path.push(TraceNode::typed(
1820        if operation.is_select() {
1821            TraceKind::Request
1822        } else {
1823            TraceKind::Entity
1824        },
1825        entity,
1826        None,
1827        "",
1828    ));
1829    path.extend(
1830        source
1831            .iter()
1832            .filter(|node| node.kind == TraceKind::Relation)
1833            .cloned(),
1834    );
1835    path.push(TraceNode::typed(
1836        TraceKind::Provider,
1837        if backend.trim().is_empty() {
1838            "unknown"
1839        } else {
1840            backend
1841        },
1842        None,
1843        "",
1844    ));
1845    path.push(TraceNode::typed(TraceKind::Sql, statement, None, ""));
1846    path
1847}
1848
1849fn pretty_sql(sql: &str) -> String {
1850    let mut pretty = sql.to_owned();
1851    for keyword in [
1852        " FROM ",
1853        " WHERE ",
1854        " GROUP BY ",
1855        " HAVING ",
1856        " ORDER BY ",
1857        " LIMIT ",
1858        " OFFSET ",
1859        " RETURNING ",
1860    ] {
1861        pretty = pretty.replace(keyword, &format!("\n{}", keyword.trim_start()));
1862    }
1863    pretty.replace(" AND ", "\n  AND ")
1864}
1865
1866#[cfg(test)]
1867mod sql_log_option_tests {
1868    use super::*;
1869
1870    #[test]
1871    fn diagnostic_sql_log_is_enabled_by_default_with_independent_switches() {
1872        let mut context = UserContext::default();
1873        assert_eq!(context.sql_log_options(), SqlLogOptions::all());
1874        assert!(context.sql_logs().is_empty());
1875
1876        context.disable_select_sql_log();
1877        assert_eq!(context.sql_log_options(), SqlLogOptions::mutation_only());
1878
1879        context.enable_select_sql_log();
1880        context.disable_mutation_sql_log();
1881        assert_eq!(context.sql_log_options(), SqlLogOptions::select_only());
1882
1883        context.disable_sql_log();
1884        assert_eq!(context.sql_log_options(), SqlLogOptions::disabled());
1885    }
1886
1887    #[test]
1888    fn disabled_sql_log_rejects_executor_metadata_before_recording() {
1889        let mut context = UserContext::default();
1890        context.disable_sql_log();
1891        let now = SystemTime::now();
1892        context.record_metadata_log(&teaql_data_service::ExecutionMetadata {
1893            backend: "sql".to_owned(),
1894            operation: teaql_data_service::DataServiceOperation::Query,
1895            started_at: now,
1896            ended_at: now,
1897            affected_rows: None,
1898            result_count: Some(1),
1899            trace_chain: Vec::new(),
1900            comment: Some("disabled log test".to_owned()),
1901            backend_request_id: None,
1902            parameterized_query: Some("SELECT id FROM sample WHERE id = $1".to_owned()),
1903            params: vec![Value::I64(1)],
1904            debug_query: Some("SELECT id FROM sample WHERE id = 1".to_owned()),
1905        });
1906        assert!(context.sql_logs().is_empty());
1907    }
1908
1909    #[test]
1910    fn metadata_log_retains_structured_intent_sql_forms_and_multilevel_trace() {
1911        let context = UserContext::default();
1912        let now = SystemTime::now();
1913        let query_trace = vec![
1914            teaql_core::TraceNode::typed(
1915                teaql_core::TraceKind::Comment,
1916                "School",
1917                None,
1918                "what: load school graph",
1919            ),
1920            teaql_core::TraceNode::typed(
1921                teaql_core::TraceKind::Purpose,
1922                "School",
1923                None,
1924                "why: render school details",
1925            ),
1926            teaql_core::TraceNode::typed(
1927                teaql_core::TraceKind::Relation,
1928                "platform",
1929                None,
1930                "School.platform",
1931            ),
1932            teaql_core::TraceNode::typed(
1933                teaql_core::TraceKind::Relation,
1934                "organization",
1935                None,
1936                "Platform.organization",
1937            ),
1938            teaql_core::TraceNode::typed(
1939                teaql_core::TraceKind::Relation,
1940                "region",
1941                None,
1942                "Organization.region",
1943            ),
1944        ];
1945        context.record_metadata_log(&teaql_data_service::ExecutionMetadata {
1946            backend: "sqlite".to_owned(),
1947            operation: teaql_data_service::DataServiceOperation::Query,
1948            started_at: now,
1949            ended_at: now,
1950            affected_rows: None,
1951            result_count: Some(2),
1952            trace_chain: query_trace.clone(),
1953            comment: None,
1954            backend_request_id: None,
1955            parameterized_query: Some("SELECT name FROM school_data WHERE id = ?".to_owned()),
1956            params: vec![Value::I64(7)],
1957            debug_query: Some("SELECT name FROM school_data WHERE id = 7".to_owned()),
1958        });
1959
1960        let query = context.sql_logs().pop().expect("query log");
1961        assert_eq!(query.comment.as_deref(), Some("what: load school graph"));
1962        assert_eq!(query.purpose.as_deref(), Some("why: render school details"));
1963        assert_eq!(query.audit_reason, None);
1964        assert_eq!(
1965            query
1966                .trace_path
1967                .iter()
1968                .map(|node| node.kind)
1969                .collect::<Vec<_>>(),
1970            vec![
1971                teaql_core::TraceKind::Operation,
1972                teaql_core::TraceKind::Request,
1973                teaql_core::TraceKind::Relation,
1974                teaql_core::TraceKind::Relation,
1975                teaql_core::TraceKind::Relation,
1976                teaql_core::TraceKind::Provider,
1977                teaql_core::TraceKind::Sql,
1978            ]
1979        );
1980        assert_eq!(query.trace_path[0].entity_type, "School");
1981        assert_eq!(query.trace_path[5].entity_type, "sqlite");
1982        assert_eq!(query.trace_path[6].entity_type, "select");
1983        assert_eq!(query.sql, "SELECT name FROM school_data WHERE id = ?");
1984        assert_eq!(query.params, vec![Value::I64(7)]);
1985        assert_eq!(query.debug_sql, "SELECT name FROM school_data WHERE id = 7");
1986        assert_eq!(query.result_count, Some(2));
1987
1988        let mutation_trace = vec![teaql_core::TraceNode::typed(
1989            teaql_core::TraceKind::AuditReason,
1990            "School",
1991            Some(7),
1992            "correct school name",
1993        )];
1994        context.record_metadata_log(&teaql_data_service::ExecutionMetadata {
1995            backend: "sqlite".to_owned(),
1996            operation: teaql_data_service::DataServiceOperation::Update,
1997            started_at: now,
1998            ended_at: now,
1999            affected_rows: Some(1),
2000            result_count: None,
2001            trace_chain: mutation_trace.clone(),
2002            comment: None,
2003            backend_request_id: None,
2004            parameterized_query: Some("UPDATE school_data SET name = ? WHERE id = ?".to_owned()),
2005            params: vec![Value::from("Academy"), Value::I64(7)],
2006            debug_query: Some("UPDATE school_data SET name = 'Academy' WHERE id = 7".to_owned()),
2007        });
2008        let mutation = context.sql_logs().pop().expect("mutation log");
2009        assert_eq!(mutation.comment, None);
2010        assert_eq!(mutation.purpose, None);
2011        assert_eq!(
2012            mutation.audit_reason.as_deref(),
2013            Some("correct school name")
2014        );
2015        assert_eq!(
2016            mutation
2017                .trace_path
2018                .iter()
2019                .map(|node| node.kind)
2020                .collect::<Vec<_>>(),
2021            vec![
2022                teaql_core::TraceKind::Operation,
2023                teaql_core::TraceKind::Entity,
2024                teaql_core::TraceKind::Provider,
2025                teaql_core::TraceKind::Sql,
2026            ]
2027        );
2028        assert_eq!(mutation.affected_rows, Some(1));
2029    }
2030}
2031
2032#[cfg(test)]
2033mod entity_runtime_state_tests {
2034    use super::*;
2035    use crate::EntityKey;
2036
2037    #[test]
2038    fn reused_user_context_returns_independent_mutation_ledgers() {
2039        let context = UserContext::default();
2040        let first = context.entity_runtime_state();
2041        let key = EntityKey::new("School", 1_u64);
2042        first.set(key.clone(), "name", "First");
2043
2044        let second = context.entity_runtime_state();
2045
2046        assert_eq!(first.changed_field_names(&key).len(), 1);
2047        assert!(second.changed_field_names(&key).is_empty());
2048    }
2049}