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