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