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