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