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