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