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