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