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, Value};
11use teaql_sql::{CompiledQuery, DatabaseKind};
12
13use crate::EntityRuntimeState;
14use crate::{
15 CheckObjectStatus, CheckResult, CheckResults, CheckerRegistry, ContextError,
16 EntityDataServiceBehavior, EntityDataServiceBehaviorRegistry, EntityGraphBuilder,
17 EntityRegistry, GraphNode, InMemoryEntityGraphDecoderRegistry, InternalIdGenerator, Language,
18 MetadataStore, ObjectLocation, RawAuditEvent, RawAuditEventSink, RequestPolicy, RuntimeError,
19 local_id_generator,
20};
21
22tokio::task_local! {
23 static GENERATED_SCHEMA_BOOTSTRAP_MODE: ();
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ContextEntityRef {
28 pub entity_type: String,
29 pub id: u64,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ContextRootError {
34 pub expected_entity_type: String,
35 pub actual_root: Option<ContextEntityRef>,
36}
37
38impl std::fmt::Display for ContextRootError {
39 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match &self.actual_root {
41 None => write!(
42 formatter,
43 "active root {} is missing from UserContext",
44 self.expected_entity_type
45 ),
46 Some(actual) => write!(
47 formatter,
48 "active root type is {}, expected {}",
49 actual.entity_type, self.expected_entity_type
50 ),
51 }
52 }
53}
54
55impl std::error::Error for ContextRootError {}
56
57#[cfg(test)]
58mod active_root_tests {
59 use super::UserContext;
60
61 #[test]
62 fn active_root_is_typed_and_fails_closed() {
63 let context = UserContext::new().with_active_root("Tenant", 42);
64 assert_eq!(context.require_active_root("Tenant").unwrap().id, 42);
65 assert!(context.require_active_root("Organization").is_err());
66 assert!(UserContext::new().require_active_root("Tenant").is_err());
67 }
68}
69
70#[derive(Debug, Clone, PartialEq)]
71pub struct ContinuousPageCursor {
72 pub cursor_id: String,
73 pub query_key: String,
74 pub entity: String,
75 pub direction: teaql_core::SortDirection,
76 pub boundary: Value,
77 pub page_size: u64,
78 pub next_offset: u64,
79 pub expires_at: SystemTime,
80}
81
82#[async_trait::async_trait]
83pub trait ContinuousPageCursorStore: Send + Sync + 'static {
84 async fn get(
85 &self,
86 query_key: &str,
87 target_offset: u64,
88 ) -> Result<Option<ContinuousPageCursor>, String>;
89 async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String>;
90 async fn invalidate(&self, query_key: &str) -> Result<(), String>;
91}
92
93pub struct InMemoryContinuousPageCursorStore {
94 cursors: Mutex<HashMap<String, ContinuousPageCursor>>,
95 max_entries: usize,
96}
97
98#[derive(Debug, Clone)]
99pub struct RetainedIdSet {
100 pub query_key: String,
101 pub ids: Arc<Vec<u64>>,
102 pub expires_at: SystemTime,
103}
104
105#[async_trait::async_trait]
106pub trait IdSetStore: Send + Sync + 'static {
107 async fn get(&self, query_key: &str) -> Result<Option<RetainedIdSet>, String>;
108 async fn put(&self, id_set: RetainedIdSet) -> Result<(), String>;
109 async fn invalidate(&self, query_key: &str) -> Result<(), String>;
110}
111
112pub struct InMemoryIdSetStore {
113 sets: Mutex<HashMap<String, RetainedIdSet>>,
114 max_entries: usize,
115 max_bytes: usize,
116}
117
118impl Default for InMemoryIdSetStore {
119 fn default() -> Self {
120 Self {
121 sets: Mutex::new(HashMap::new()),
122 max_entries: 64,
123 max_bytes: 256 * 1024 * 1024,
124 }
125 }
126}
127
128impl InMemoryIdSetStore {
129 fn retained_bytes(sets: &HashMap<String, RetainedIdSet>) -> usize {
130 sets.values()
131 .map(|value| value.ids.len().saturating_mul(std::mem::size_of::<u64>()))
132 .sum()
133 }
134}
135
136#[async_trait::async_trait]
137impl IdSetStore for InMemoryIdSetStore {
138 async fn get(&self, query_key: &str) -> Result<Option<RetainedIdSet>, String> {
139 let mut sets = self.sets.lock().map_err(|error| error.to_string())?;
140 if sets
141 .get(query_key)
142 .is_some_and(|value| value.expires_at <= SystemTime::now())
143 {
144 sets.remove(query_key);
145 }
146 Ok(sets.get(query_key).cloned())
147 }
148
149 async fn put(&self, id_set: RetainedIdSet) -> Result<(), String> {
150 let incoming_bytes = id_set.ids.len().saturating_mul(std::mem::size_of::<u64>());
151 if incoming_bytes > self.max_bytes {
152 return Err("ID set exceeds the process-local store memory ceiling".to_owned());
153 }
154 let mut sets = self.sets.lock().map_err(|error| error.to_string())?;
155 sets.retain(|_, value| value.expires_at > SystemTime::now());
156 while sets.len() >= self.max_entries
157 || Self::retained_bytes(&sets).saturating_add(incoming_bytes) > self.max_bytes
158 {
159 let Some(oldest) = sets
160 .iter()
161 .min_by_key(|(_, value)| value.expires_at)
162 .map(|(key, _)| key.clone())
163 else {
164 break;
165 };
166 sets.remove(&oldest);
167 }
168 sets.insert(id_set.query_key.clone(), id_set);
169 Ok(())
170 }
171
172 async fn invalidate(&self, query_key: &str) -> Result<(), String> {
173 self.sets
174 .lock()
175 .map_err(|error| error.to_string())?
176 .remove(query_key);
177 Ok(())
178 }
179}
180
181fn id_set_build_lock(query_key: &str) -> Arc<futures_util::lock::Mutex<()>> {
182 static LOCKS: OnceLock<Mutex<HashMap<String, std::sync::Weak<futures_util::lock::Mutex<()>>>>> =
183 OnceLock::new();
184 let mut locks = LOCKS
185 .get_or_init(|| Mutex::new(HashMap::new()))
186 .lock()
187 .expect("ID set build lock registry poisoned");
188 locks.retain(|_, lock| lock.strong_count() > 0);
189 if let Some(lock) = locks.get(query_key).and_then(std::sync::Weak::upgrade) {
190 return lock;
191 }
192 let lock = Arc::new(futures_util::lock::Mutex::new(()));
193 locks.insert(query_key.to_owned(), Arc::downgrade(&lock));
194 lock
195}
196
197impl Default for InMemoryContinuousPageCursorStore {
198 fn default() -> Self {
199 Self {
200 cursors: Mutex::new(HashMap::new()),
201 max_entries: 4096,
202 }
203 }
204}
205
206#[async_trait::async_trait]
207impl ContinuousPageCursorStore for InMemoryContinuousPageCursorStore {
208 async fn get(
209 &self,
210 query_key: &str,
211 target_offset: u64,
212 ) -> Result<Option<ContinuousPageCursor>, String> {
213 let key = format!("{query_key}:{target_offset}");
214 let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
215 if cursors
216 .get(&key)
217 .is_some_and(|cursor| cursor.expires_at <= SystemTime::now())
218 {
219 cursors.remove(&key);
220 }
221 Ok(cursors.get(&key).cloned())
222 }
223
224 async fn put(&self, cursor: ContinuousPageCursor) -> Result<(), String> {
225 let key = format!("{}:{}", cursor.query_key, cursor.next_offset);
226 let mut cursors = self.cursors.lock().map_err(|e| e.to_string())?;
227 if cursors.len() >= self.max_entries {
228 if let Some(expired_or_oldest) = cursors
229 .iter()
230 .min_by_key(|(_, value)| value.expires_at)
231 .map(|(key, _)| key.clone())
232 {
233 cursors.remove(&expired_or_oldest);
234 }
235 }
236 cursors.insert(key, cursor);
237 Ok(())
238 }
239
240 async fn invalidate(&self, query_key: &str) -> Result<(), String> {
241 let prefix = format!("{query_key}:");
242 self.cursors
243 .lock()
244 .map_err(|e| e.to_string())?
245 .retain(|key, _| !key.starts_with(&prefix));
246 Ok(())
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum SqlLogOperation {
252 Select,
253 Insert,
254 Update,
255 Delete,
256 Recover,
257}
258
259impl SqlLogOperation {
260 pub fn is_select(self) -> bool {
261 matches!(self, Self::Select)
262 }
263
264 pub fn is_mutation(self) -> bool {
265 !self.is_select()
266 }
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub struct SqlLogOptions {
271 pub select: bool,
272 pub mutation: bool,
273}
274
275impl Default for SqlLogOptions {
276 fn default() -> Self {
277 Self::all()
278 }
279}
280
281impl SqlLogOptions {
282 pub fn disabled() -> Self {
283 Self {
284 select: false,
285 mutation: false,
286 }
287 }
288
289 pub fn select_only() -> Self {
290 Self {
291 select: true,
292 mutation: false,
293 }
294 }
295
296 pub fn mutation_only() -> Self {
297 Self {
298 select: false,
299 mutation: true,
300 }
301 }
302
303 pub fn all() -> Self {
304 Self {
305 select: true,
306 mutation: true,
307 }
308 }
309
310 pub fn enabled_for(self, operation: SqlLogOperation) -> bool {
311 match operation.is_select() {
312 true => self.select,
313 false => self.mutation,
314 }
315 }
316}
317
318#[derive(Debug, Clone, PartialEq)]
319pub struct SqlLogEntry {
320 pub operation: SqlLogOperation,
321 pub comment: Option<String>,
322 pub purpose: Option<String>,
323 pub audit_reason: Option<String>,
324 pub trace_path: Vec<teaql_core::TraceNode>,
325 pub sql: String,
326 pub params: Vec<Value>,
327 pub debug_sql: String,
328 pub pretty_sql: String,
329 pub started_at: SystemTime,
330 pub ended_at: SystemTime,
331 pub elapsed: Duration,
332 pub result_count: Option<usize>,
333 pub result_type: Option<String>,
334 pub affected_rows: Option<u64>,
335 pub result_summary: String,
336}
337
338#[derive(Debug, Clone, PartialEq)]
339pub struct UnifiedLogEntry {
340 pub timestamp: SystemTime,
341 pub user_identifier: Option<String>,
342 pub trace_chain: Vec<teaql_core::TraceNode>,
343 pub payload: LogPayload,
344}
345
346#[derive(Debug, Clone, PartialEq)]
347pub enum LogPayload {
348 Sql(SqlLogEntry),
349 Info(InfoLogEntry),
350}
351
352#[derive(Debug, Clone, PartialEq)]
353pub struct InfoLogEntry {
354 pub message: String,
355}
356
357#[derive(Clone, Default)]
358pub struct UnifiedLogBuffer {
359 pub entries: std::sync::Arc<Mutex<Vec<UnifiedLogEntry>>>,
360}
361
362pub struct SchemaInvocation {
369 _context_owned: (),
370}
371
372pub trait SchemaProvider: Send + Sync {
373 fn ensure_schema<'a>(
374 &'a self,
375 context: &'a UserContext,
376 invocation: &'a SchemaInvocation,
377 ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>>;
378}
379
380pub type GeneratedSchemaBootstrapFuture<'a> =
381 Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>>;
382pub type GeneratedSchemaBootstrap =
383 for<'a> fn(&'a UserContext) -> GeneratedSchemaBootstrapFuture<'a>;
384
385#[derive(Clone, Debug, PartialEq, Eq)]
386pub enum FixEvidenceSource {
387 Clock,
388 Context,
389}
390
391#[derive(Clone, Debug, PartialEq, Eq)]
392pub struct FixEvidence {
393 pub entity_type: String,
394 pub model_path: String,
395 pub source: FixEvidenceSource,
396 pub source_label: String,
397}
398
399impl FixEvidence {
400 pub fn new(
401 entity_type: &str,
402 model_path: &str,
403 source: FixEvidenceSource,
404 source_label: &str,
405 ) -> Self {
406 assert!(
407 !entity_type.trim().is_empty(),
408 "entity_type must not be blank"
409 );
410 assert!(
411 !model_path.trim().is_empty(),
412 "model_path must not be blank"
413 );
414 assert!(
415 !source_label.trim().is_empty(),
416 "source_label must not be blank"
417 );
418 let normalized = source_label.to_ascii_lowercase();
419 assert!(
420 !normalized.contains("authorization")
421 && !normalized.contains("cookie")
422 && !normalized.contains("token="),
423 "source_label must be a safe framework label"
424 );
425 Self {
426 entity_type: entity_type.to_owned(),
427 model_path: model_path.to_owned(),
428 source,
429 source_label: source_label.to_owned(),
430 }
431 }
432}
433
434pub struct UserContext {
435 active_root: OnceLock<ContextEntityRef>,
436 pub(crate) metadata: Option<Box<dyn MetadataStore>>,
437 pub(crate) entity_registry: Option<Box<dyn EntityRegistry>>,
438 pub(crate) entity_graph_decoders: InMemoryEntityGraphDecoderRegistry,
439 pub(crate) entity_data_service_behavior_registry:
440 Option<Box<dyn EntityDataServiceBehaviorRegistry>>,
441 pub(crate) request_policy: Option<Box<dyn RequestPolicy>>,
442 pub(crate) checker_registry: Option<Box<dyn CheckerRegistry>>,
443 pub(crate) event_sink: Option<Box<dyn RawAuditEventSink>>,
444 pub(crate) custom_event_sink: Option<Box<dyn crate::SafeAuditEventSink>>,
445 pub(crate) internal_id_generator: Option<Box<dyn InternalIdGenerator>>,
446 schema_provider: Option<Box<dyn SchemaProvider>>,
447 generated_schema_bootstraps: Vec<GeneratedSchemaBootstrap>,
448 language: Language,
449 i18n_catalog: Arc<crate::I18nCatalog>,
450 typed_resources: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
451 named_resources: BTreeMap<String, Box<dyn Any + Send + Sync>>,
452 locals: BTreeMap<String, Value>,
453 pub(crate) initial_graphs: Vec<GraphNode>,
454 pub(crate) root_graphs: Vec<GraphNode>,
455 entity_runtime_state: EntityRuntimeState,
456 sql_log_options: SqlLogOptions,
457 sql_log_entries: Mutex<Vec<SqlLogEntry>>,
458 user_identifier: Option<String>,
459 timezone: Option<String>,
460 trace_id: String,
461 continuous_page_cursor_store: std::sync::Arc<dyn ContinuousPageCursorStore>,
462 continuous_page_observation: Mutex<(String, Option<String>)>,
463 id_set_store: Arc<dyn IdSetStore>,
464 id_set_observation: Mutex<(String, Option<u64>)>,
465 local_lock_owner: u64,
466 remote_lock_owner: String,
467 runtime_telemetry: Arc<dyn crate::RuntimeTelemetry>,
468 last_fix_evidence: Mutex<Vec<FixEvidence>>,
469}
470
471#[derive(Clone, Copy)]
472struct LocalLockEntry {
473 owner: u64,
474 expires_at: Option<Instant>,
475}
476
477#[derive(Default)]
478struct ProcessLocalLocks {
479 entries: Mutex<HashMap<String, LocalLockEntry>>,
480 changed: Condvar,
481}
482
483static PROCESS_LOCAL_LOCKS: OnceLock<ProcessLocalLocks> = OnceLock::new();
484static NEXT_LOCAL_LOCK_OWNER: AtomicU64 = AtomicU64::new(1);
485
486impl Default for UserContext {
487 fn default() -> Self {
488 let pid = std::process::id();
489 let thread_id_str = format!("{:?}", std::thread::current().id());
490 let numeric_thread_id = thread_id_str
491 .strip_prefix("ThreadId(")
492 .and_then(|s| s.strip_suffix(")"))
493 .unwrap_or(&thread_id_str);
494 let os_user = std::env::var("USER")
495 .or_else(|_| std::env::var("USERNAME"))
496 .unwrap_or_else(|_| "main".to_owned());
497 let user_id = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
498 let owner_sequence = NEXT_LOCAL_LOCK_OWNER.fetch_add(1, Ordering::Relaxed);
499 Self {
500 active_root: OnceLock::new(),
501 metadata: None,
502 entity_registry: None,
503 entity_graph_decoders: InMemoryEntityGraphDecoderRegistry::default(),
504 entity_data_service_behavior_registry: None,
505 request_policy: None,
506 checker_registry: None,
507 event_sink: None,
508 custom_event_sink: None,
509 internal_id_generator: None,
510 schema_provider: None,
511 language: Language::default(),
512 i18n_catalog: crate::I18nCatalog::builtin().clone(),
513 typed_resources: HashMap::new(),
514 named_resources: BTreeMap::new(),
515 locals: BTreeMap::new(),
516 initial_graphs: Vec::new(),
517 root_graphs: Vec::new(),
518 generated_schema_bootstraps: Vec::new(),
519 entity_runtime_state: EntityRuntimeState::default(),
520 sql_log_options: SqlLogOptions::default(),
524 sql_log_entries: Mutex::new(Vec::new()),
525 user_identifier: Some(user_id),
526 timezone: Some("UTC".to_owned()),
527 trace_id: format!(
528 "req-{pid}-{numeric_thread_id}-{:x}",
529 std::time::SystemTime::now()
530 .duration_since(std::time::UNIX_EPOCH)
531 .unwrap_or_default()
532 .as_micros()
533 ),
534 continuous_page_cursor_store: std::sync::Arc::new(
535 InMemoryContinuousPageCursorStore::default(),
536 ),
537 continuous_page_observation: Mutex::new(("DISABLED".to_owned(), None)),
538 id_set_store: Arc::new(InMemoryIdSetStore::default()),
539 id_set_observation: Mutex::new(("ID_SET_DISABLED".to_owned(), None)),
540 local_lock_owner: owner_sequence,
541 remote_lock_owner: format!(
542 "teaql:{pid}:{owner_sequence}:{}",
543 SystemTime::now()
544 .duration_since(SystemTime::UNIX_EPOCH)
545 .unwrap_or_default()
546 .as_nanos()
547 ),
548 runtime_telemetry: Arc::new(crate::NoopRuntimeTelemetry),
549 last_fix_evidence: Mutex::new(Vec::new()),
550 }
551 }
552}
553
554#[async_trait::async_trait]
555pub trait DataStore: Send + Sync + 'static {
556 async fn get(&self, key: &str) -> Option<Value>;
557 async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>);
558 async fn remove(&self, key: &str);
559}
560
561#[async_trait::async_trait]
567pub trait RemoteLockProvider: Send + Sync + 'static {
568 async fn try_remote_lock(
569 &self,
570 key: &str,
571 owner_token: &str,
572 timeout_millis: u64,
573 expire_millis: u64,
574 ) -> bool;
575
576 async fn unlock_remote(&self, key: &str, owner_token: &str) -> bool;
577}
578
579#[derive(Default)]
580pub struct InMemoryDataStore {
581 cache: std::sync::RwLock<HashMap<String, (Value, Option<std::time::Instant>)>>,
582}
583
584#[async_trait::async_trait]
585impl DataStore for InMemoryDataStore {
586 async fn get(&self, key: &str) -> Option<Value> {
587 let lock = self.cache.read().unwrap();
588 if let Some((val, expires_at)) = lock.get(key) {
589 if let Some(exp) = expires_at {
590 if std::time::Instant::now() > *exp {
591 return None;
592 }
593 }
594 return Some(val.clone());
595 }
596 None
597 }
598
599 async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>) {
600 let mut lock = self.cache.write().unwrap();
601 let expires_at = timeout_seconds
602 .map(|secs| std::time::Instant::now() + std::time::Duration::from_secs(secs));
603 lock.insert(key.to_string(), (value, expires_at));
604 }
605
606 async fn remove(&self, key: &str) {
607 let mut lock = self.cache.write().unwrap();
608 lock.remove(key);
609 }
610}
611
612impl UserContext {
613 pub fn new() -> Self {
614 Self::default()
615 }
616
617 pub fn with_active_root(mut self, entity_type: impl Into<String>, id: u64) -> Self {
618 let entity_type = crate::canonical_id_space_entity(&entity_type.into());
619 assert!(
620 !entity_type.trim().is_empty(),
621 "active root entity type is required"
622 );
623 assert!(id > 0, "active root id must be positive");
624 self.active_root
625 .set(ContextEntityRef { entity_type, id })
626 .expect("active root may only be assigned once");
627 self
628 }
629
630 #[doc(hidden)]
631 pub fn set_generated_bootstrap_active_root(
632 &self,
633 entity_type: impl Into<String>,
634 id: u64,
635 ) -> Result<(), RuntimeError> {
636 let entity_type = crate::canonical_id_space_entity(&entity_type.into());
637 if entity_type.trim().is_empty() || id == 0 {
638 return Err(RuntimeError::Schema(
639 "invalid generated active root".to_owned(),
640 ));
641 }
642 match self.active_root.get() {
643 Some(existing) if existing.entity_type == entity_type && existing.id == id => Ok(()),
644 Some(existing) => Err(RuntimeError::Schema(format!(
645 "active root already set to {}:{}",
646 existing.entity_type, existing.id
647 ))),
648 None => self
649 .active_root
650 .set(ContextEntityRef { entity_type, id })
651 .map_err(|_| RuntimeError::Schema("active root initialization raced".to_owned())),
652 }
653 }
654
655 pub fn require_active_root(
656 &self,
657 expected_entity_type: &str,
658 ) -> Result<&ContextEntityRef, ContextRootError> {
659 let canonical_expected = crate::canonical_id_space_entity(expected_entity_type);
660 match self.active_root.get() {
661 Some(root) if root.entity_type == canonical_expected => Ok(root),
662 actual_root => Err(ContextRootError {
663 expected_entity_type: canonical_expected,
664 actual_root: actual_root.cloned(),
665 }),
666 }
667 }
668
669 pub(crate) fn active_root_ref(&self) -> Option<&ContextEntityRef> {
670 self.active_root.get()
671 }
672
673 pub fn with_runtime_telemetry(mut self, telemetry: Arc<dyn crate::RuntimeTelemetry>) -> Self {
674 self.runtime_telemetry = telemetry;
675 self
676 }
677
678 pub fn set_runtime_telemetry(&mut self, telemetry: Arc<dyn crate::RuntimeTelemetry>) {
679 self.runtime_telemetry = telemetry;
680 }
681
682 pub fn runtime_telemetry(&self) -> &Arc<dyn crate::RuntimeTelemetry> {
683 &self.runtime_telemetry
684 }
685
686 pub(crate) fn runtime_telemetry_is_noop(&self) -> bool {
687 self.runtime_telemetry.is_noop()
688 }
689
690 pub fn start_runtime_operation(
691 &self,
692 operation: crate::RuntimeOperation,
693 ) -> crate::FailOpenRuntimeTelemetryScope {
694 crate::start_runtime_operation(&self.runtime_telemetry, operation)
695 }
696
697 pub fn try_local_lock(&self, key: &str, timeout_millis: u64, expire_millis: u64) -> bool {
698 let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
699 let deadline = Instant::now() + Duration::from_millis(timeout_millis);
700 let mut entries = locks.entries.lock().expect("local lock state poisoned");
701 loop {
702 let now = Instant::now();
703 match entries.get(key).copied() {
704 None => {
705 entries.insert(
706 key.to_owned(),
707 LocalLockEntry {
708 owner: self.local_lock_owner,
709 expires_at: (expire_millis > 0)
710 .then(|| now + Duration::from_millis(expire_millis)),
711 },
712 );
713 return true;
714 }
715 Some(current)
716 if current.owner == self.local_lock_owner
717 || current.expires_at.is_some_and(|expiry| now >= expiry) =>
718 {
719 entries.insert(
720 key.to_owned(),
721 LocalLockEntry {
722 owner: self.local_lock_owner,
723 expires_at: (expire_millis > 0)
724 .then(|| now + Duration::from_millis(expire_millis)),
725 },
726 );
727 return true;
728 }
729 Some(current) => {
730 if timeout_millis == 0 || now >= deadline {
731 return false;
732 }
733 let wake_after = current
734 .expires_at
735 .map(|expiry| expiry.saturating_duration_since(now))
736 .unwrap_or_else(|| deadline.saturating_duration_since(now))
737 .min(deadline.saturating_duration_since(now));
738 let waited = locks
739 .changed
740 .wait_timeout(entries, wake_after)
741 .expect("local lock state poisoned");
742 entries = waited.0;
743 }
744 }
745 }
746 }
747
748 pub fn unlock_local(&self, key: &str) {
749 let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
750 let mut entries = locks.entries.lock().expect("local lock state poisoned");
751 if entries
752 .get(key)
753 .is_some_and(|entry| entry.owner == self.local_lock_owner)
754 {
755 entries.remove(key);
756 locks.changed.notify_all();
757 }
758 }
759
760 pub async fn try_remote_lock(
766 &self,
767 key: &str,
768 timeout_millis: u64,
769 expire_millis: u64,
770 ) -> bool {
771 match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
772 Some(provider) => {
773 provider
774 .try_remote_lock(key, &self.remote_lock_owner, timeout_millis, expire_millis)
775 .await
776 }
777 None => true,
778 }
779 }
780
781 pub async fn unlock_remote(&self, key: &str) -> bool {
783 match self.get_resource::<Arc<dyn RemoteLockProvider>>() {
784 Some(provider) => provider.unlock_remote(key, &self.remote_lock_owner).await,
785 None => true,
786 }
787 }
788
789 pub fn user_identifier(&self) -> Option<&str> {
790 self.user_identifier.as_deref()
791 }
792
793 pub fn set_user_identifier(&mut self, user_identifier: impl Into<String>) {
794 self.user_identifier = Some(user_identifier.into());
795 }
796
797 pub fn set_continuous_page_cursor_store(
798 &mut self,
799 store: std::sync::Arc<dyn ContinuousPageCursorStore>,
800 ) {
801 self.continuous_page_cursor_store = store;
802 }
803
804 pub fn continuous_page_plan(&self) -> Option<String> {
805 self.continuous_page_observation
806 .lock()
807 .ok()
808 .map(|value| value.0.clone())
809 }
810
811 pub fn continuous_page_cursor_id(&self) -> Option<String> {
812 self.continuous_page_observation
813 .lock()
814 .ok()
815 .and_then(|value| value.1.clone())
816 }
817
818 pub(crate) fn observe_continuous_page(
819 &self,
820 plan: impl Into<String>,
821 cursor_id: Option<String>,
822 ) {
823 if let Ok(mut observation) = self.continuous_page_observation.lock() {
824 *observation = (plan.into(), cursor_id);
825 }
826 }
827
828 pub(crate) fn continuous_page_cursor_store(&self) -> &dyn ContinuousPageCursorStore {
829 self.continuous_page_cursor_store.as_ref()
830 }
831
832 pub fn set_id_set_store(&mut self, store: Arc<dyn IdSetStore>) {
833 self.id_set_store = store;
834 }
835
836 pub fn id_set_plan(&self) -> Option<String> {
837 self.id_set_observation
838 .lock()
839 .ok()
840 .map(|observation| observation.0.clone())
841 }
842
843 pub fn id_set_count(&self) -> Option<u64> {
844 self.id_set_observation
845 .lock()
846 .ok()
847 .and_then(|observation| observation.1)
848 }
849
850 pub(crate) fn observe_id_set(&self, plan: impl Into<String>, count: Option<u64>) {
851 if let Ok(mut observation) = self.id_set_observation.lock() {
852 *observation = (plan.into(), count);
853 }
854 }
855
856 pub(crate) fn id_set_store(&self) -> &dyn IdSetStore {
857 self.id_set_store.as_ref()
858 }
859
860 pub(crate) fn id_set_build_lock(&self, query_key: &str) -> Arc<futures_util::lock::Mutex<()>> {
861 id_set_build_lock(query_key)
862 }
863
864 pub fn with_user_identifier(mut self, user_identifier: impl Into<String>) -> Self {
865 self.user_identifier = Some(user_identifier.into());
866 self
867 }
868
869 pub fn set_user_identifier_option(&mut self, user_identifier: Option<String>) {
870 self.user_identifier = user_identifier;
871 }
872
873 pub fn with_user_identifier_option(mut self, user_identifier: Option<String>) -> Self {
874 self.user_identifier = user_identifier;
875 self
876 }
877
878 pub fn timezone(&self) -> Option<&str> {
879 self.timezone.as_deref()
880 }
881
882 pub fn set_timezone(&mut self, timezone: impl Into<String>) {
883 self.timezone = Some(timezone.into());
884 }
885
886 pub fn with_timezone(mut self, timezone: impl Into<String>) -> Self {
887 self.timezone = Some(timezone.into());
888 self
889 }
890
891 pub fn trace_id(&self) -> &str {
892 &self.trace_id
893 }
894
895 pub fn set_trace_id(&mut self, trace_id: impl Into<String>) {
896 self.trace_id = trace_id.into();
897 }
898
899 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
900 self.trace_id = trace_id.into();
901 self
902 }
903
904 pub fn with_module(mut self, module: crate::RuntimeModule) -> Self {
905 module.apply_to(&mut self);
906 self
907 }
908
909 pub fn entity_runtime_state(&self) -> EntityRuntimeState {
910 EntityRuntimeState::fresh_with_shared_graph(&self.entity_runtime_state)
913 }
914
915 pub fn initial_graphs(&self) -> &[GraphNode] {
916 &self.initial_graphs
917 }
918
919 pub fn set_initial_graphs(&mut self, graphs: Vec<GraphNode>) {
920 self.initial_graphs = graphs;
921 }
922
923 pub fn root_graphs(&self) -> &[GraphNode] {
924 &self.root_graphs
925 }
926
927 pub fn set_root_graphs(&mut self, graphs: Vec<GraphNode>) {
928 self.root_graphs = graphs;
929 }
930
931 pub fn with_metadata(mut self, metadata: impl MetadataStore + 'static) -> Self {
932 self.metadata = Some(Box::new(metadata));
933 self
934 }
935
936 pub fn set_metadata(&mut self, metadata: impl MetadataStore + 'static) {
937 self.metadata = Some(Box::new(metadata));
938 }
939
940 pub fn with_entity_registry(mut self, registry: impl EntityRegistry + 'static) -> Self {
941 self.entity_registry = Some(Box::new(registry));
942 self
943 }
944
945 pub fn set_entity_registry(&mut self, registry: impl EntityRegistry + 'static) {
946 self.entity_registry = Some(Box::new(registry));
947 }
948
949 pub fn set_entity_graph_decoder_registry(
950 &mut self,
951 registry: InMemoryEntityGraphDecoderRegistry,
952 ) {
953 self.entity_graph_decoders = registry;
954 }
955
956 pub(crate) fn has_entity_graph_decoder(&self, entity: &str) -> bool {
957 self.entity_graph_decoders.contains(entity)
958 }
959
960 pub(crate) fn decode_compact_entity_into_graph(
961 &self,
962 entity: &str,
963 row: teaql_core::CompactRow,
964 root: &EntityRuntimeState,
965 graph: &mut EntityGraphBuilder,
966 ) -> Result<(), teaql_core::EntityError> {
967 self.entity_graph_decoders
968 .decode_compact(entity, row, root, graph)
969 }
970
971 pub(crate) fn decode_compact_entity_list_into_graph(
972 &self,
973 entity: &str,
974 rows: Vec<teaql_core::CompactRow>,
975 root: &EntityRuntimeState,
976 graph: &mut EntityGraphBuilder,
977 owner_entity: &str,
978 owner_id: u64,
979 relation: &str,
980 ) -> Result<(), teaql_core::EntityError> {
981 self.entity_graph_decoders.decode_compact_list(
982 entity,
983 rows,
984 root,
985 graph,
986 owner_entity,
987 owner_id,
988 relation,
989 )
990 }
991
992 pub(crate) fn decode_compact_entity_batch_into_graph(
993 &self,
994 entity: &str,
995 rows: Vec<teaql_core::CompactRow>,
996 root: &EntityRuntimeState,
997 graph: &mut EntityGraphBuilder,
998 ) -> Result<(), teaql_core::EntityError> {
999 self.entity_graph_decoders
1000 .decode_compact_batch(entity, rows, root, graph)
1001 }
1002
1003 pub(crate) fn decode_compact_entity_option_into_graph(
1004 &self,
1005 entity: &str,
1006 rows: Vec<teaql_core::CompactRow>,
1007 root: &EntityRuntimeState,
1008 graph: &mut EntityGraphBuilder,
1009 owner_entity: &str,
1010 owner_id: u64,
1011 relation: &str,
1012 ) -> Result<(), teaql_core::EntityError> {
1013 self.entity_graph_decoders.decode_compact_option(
1014 entity,
1015 rows,
1016 root,
1017 graph,
1018 owner_entity,
1019 owner_id,
1020 relation,
1021 )
1022 }
1023
1024 pub fn with_entity_data_service_behavior_registry(
1025 mut self,
1026 registry: impl EntityDataServiceBehaviorRegistry + 'static,
1027 ) -> Self {
1028 self.entity_data_service_behavior_registry = Some(Box::new(registry));
1029 self
1030 }
1031
1032 pub fn set_entity_data_service_behavior_registry(
1033 &mut self,
1034 registry: impl EntityDataServiceBehaviorRegistry + 'static,
1035 ) {
1036 self.entity_data_service_behavior_registry = Some(Box::new(registry));
1037 }
1038
1039 pub fn with_request_policy(mut self, policy: impl RequestPolicy + 'static) -> Self {
1040 self.request_policy = Some(Box::new(policy));
1041 self
1042 }
1043
1044 pub fn set_request_policy(&mut self, policy: impl RequestPolicy + 'static) {
1045 self.request_policy = Some(Box::new(policy));
1046 }
1047
1048 pub fn clear_request_policy(&mut self) {
1049 self.request_policy = None;
1050 }
1051
1052 pub fn with_checker_registry(mut self, registry: impl CheckerRegistry + 'static) -> Self {
1053 self.checker_registry = Some(Box::new(registry));
1054 self
1055 }
1056
1057 pub fn set_checker_registry(&mut self, registry: impl CheckerRegistry + 'static) {
1058 self.checker_registry = Some(Box::new(registry));
1059 }
1060
1061 pub(crate) fn with_event_sink(mut self, sink: impl RawAuditEventSink + 'static) -> Self {
1062 self.event_sink = Some(Box::new(sink));
1063 self
1064 }
1065
1066 pub(crate) fn set_event_sink(&mut self, sink: impl RawAuditEventSink + 'static) {
1067 self.event_sink = Some(Box::new(sink));
1068 }
1069
1070 pub fn with_custom_event_sink(
1071 mut self,
1072 sink: impl crate::SafeAuditEventSink + 'static,
1073 ) -> Self {
1074 self.custom_event_sink = Some(Box::new(sink));
1075 self
1076 }
1077
1078 pub fn set_custom_event_sink(&mut self, sink: impl crate::SafeAuditEventSink + 'static) {
1079 self.custom_event_sink = Some(Box::new(sink));
1080 }
1081
1082 pub fn with_internal_id_generator(
1083 mut self,
1084 generator: impl InternalIdGenerator + 'static,
1085 ) -> Self {
1086 self.internal_id_generator = Some(Box::new(generator));
1087 self
1088 }
1089
1090 pub fn set_internal_id_generator(&mut self, generator: impl InternalIdGenerator + 'static) {
1091 self.internal_id_generator = Some(Box::new(generator));
1092 }
1093
1094 pub fn with_schema_provider(mut self, provider: impl SchemaProvider + 'static) -> Self {
1095 self.schema_provider = Some(Box::new(provider));
1096 self
1097 }
1098
1099 pub fn set_schema_provider(&mut self, provider: impl SchemaProvider + 'static) {
1100 self.schema_provider = Some(Box::new(provider));
1101 }
1102
1103 pub async fn ensure_schema(&self) -> Result<(), RuntimeError> {
1104 let provider = self
1105 .schema_provider
1106 .as_ref()
1107 .ok_or_else(|| RuntimeError::Schema("missing schema provider".to_owned()))?;
1108 let invocation = SchemaInvocation { _context_owned: () };
1109 provider.ensure_schema(self, &invocation).await?;
1110 GENERATED_SCHEMA_BOOTSTRAP_MODE
1111 .scope((), async {
1112 for bootstrap in &self.generated_schema_bootstraps {
1113 bootstrap(self).await?;
1114 }
1115 Ok::<(), RuntimeError>(())
1116 })
1117 .await?;
1118 Ok(())
1119 }
1120
1121 pub(crate) fn is_generated_schema_bootstrap(&self) -> bool {
1122 GENERATED_SCHEMA_BOOTSTRAP_MODE
1123 .try_with(|_| true)
1124 .unwrap_or(false)
1125 }
1126
1127 pub(crate) fn set_generated_schema_bootstraps(
1128 &mut self,
1129 bootstraps: Vec<GeneratedSchemaBootstrap>,
1130 ) {
1131 self.generated_schema_bootstraps = bootstraps;
1132 }
1133
1134 #[doc(hidden)]
1135 pub fn initialize_generated_bootstrap_entity<E: teaql_core::Entity>(
1136 &self,
1137 entity: &mut E,
1138 entity_name: &str,
1139 fixed_id: u64,
1140 ) -> Result<(), RuntimeError> {
1141 let generator = self.internal_id_generator.as_ref().ok_or_else(|| {
1142 RuntimeError::IdGeneration("missing internal ID generator".to_owned())
1143 })?;
1144 generator.ensure_floor(entity_name, fixed_id)?;
1145 entity.mark_as_new();
1146 Ok(())
1147 }
1148
1149 pub fn with_language(mut self, language: Language) -> Self {
1150 self.language = language;
1151 self
1152 }
1153
1154 pub fn set_language(&mut self, language: Language) {
1155 self.language = language;
1156 }
1157
1158 pub fn with_i18n_catalog(mut self, catalog: Arc<crate::I18nCatalog>) -> Self {
1159 self.i18n_catalog = catalog;
1160 self
1161 }
1162
1163 pub fn set_i18n_catalog(&mut self, catalog: Arc<crate::I18nCatalog>) {
1164 self.i18n_catalog = catalog;
1165 }
1166
1167 pub fn with_sql_log_options(mut self, options: SqlLogOptions) -> Self {
1168 self.sql_log_options = options;
1169 self
1170 }
1171
1172 pub fn set_sql_log_options(&mut self, options: SqlLogOptions) {
1173 self.sql_log_options = options;
1174 }
1175
1176 pub fn enable_select_sql_log(&mut self) {
1177 self.sql_log_options.select = true;
1178 }
1179
1180 pub fn enable_mutation_sql_log(&mut self) {
1181 self.sql_log_options.mutation = true;
1182 }
1183
1184 pub fn disable_select_sql_log(&mut self) {
1185 self.sql_log_options.select = false;
1186 }
1187
1188 pub fn disable_mutation_sql_log(&mut self) {
1189 self.sql_log_options.mutation = false;
1190 }
1191
1192 pub fn enable_all_sql_log(&mut self) {
1193 self.sql_log_options = SqlLogOptions::all();
1194 }
1195
1196 pub fn disable_sql_log(&mut self) {
1197 self.sql_log_options = SqlLogOptions::disabled();
1198 self.clear_sql_logs();
1199 }
1200
1201 pub fn sql_log_options(&self) -> SqlLogOptions {
1202 self.sql_log_options
1203 }
1204
1205 pub fn sql_logs(&self) -> Vec<SqlLogEntry> {
1206 self.sql_log_entries
1207 .lock()
1208 .map(|entries| entries.clone())
1209 .unwrap_or_default()
1210 }
1211
1212 pub fn clear_sql_logs(&self) {
1213 if let Ok(mut entries) = self.sql_log_entries.lock() {
1214 entries.clear();
1215 }
1216 }
1217
1218 pub(crate) fn record_sql_log(
1219 &self,
1220 operation: SqlLogOperation,
1221 query: &CompiledQuery,
1222 database_kind: DatabaseKind,
1223 started_at: SystemTime,
1224 ended_at: SystemTime,
1225 elapsed: Duration,
1226 result_count: Option<usize>,
1227 result_type: Option<String>,
1228 affected_rows: Option<u64>,
1229 trace_chain: Vec<teaql_core::TraceNode>,
1230 ) {
1231 if !self.sql_log_options.enabled_for(operation) {
1232 return;
1233 }
1234 let debug_sql = query.debug_sql(database_kind);
1235 let result_summary = sql_result_summary(
1236 operation,
1237 result_count,
1238 result_type.as_deref(),
1239 affected_rows,
1240 &debug_sql,
1241 );
1242
1243 let trace_path = canonical_sql_trace_path(
1244 operation,
1245 &format!("{database_kind:?}").to_ascii_lowercase(),
1246 &trace_chain,
1247 );
1248 let sql_log_entry = SqlLogEntry {
1249 operation,
1250 comment: trace_value(&trace_chain, teaql_core::TraceKind::Comment),
1251 purpose: trace_value(&trace_chain, teaql_core::TraceKind::Purpose),
1252 audit_reason: trace_value(&trace_chain, teaql_core::TraceKind::AuditReason),
1253 trace_path: trace_path.clone(),
1254 sql: query.sql.clone(),
1255 params: query.params.clone(),
1256 pretty_sql: pretty_sql(&debug_sql),
1257 debug_sql: debug_sql.clone(),
1258 started_at,
1259 ended_at,
1260 elapsed,
1261 result_summary: result_summary.clone(),
1262 result_count,
1263 result_type,
1264 affected_rows,
1265 };
1266
1267 if let Ok(mut entries) = self.sql_log_entries.lock() {
1268 entries.push(sql_log_entry.clone());
1269 }
1270
1271 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
1272 if let Ok(mut entries) = buf.entries.lock() {
1273 entries.push(UnifiedLogEntry {
1274 timestamp: started_at,
1275 user_identifier: self.user_identifier.clone(),
1276 trace_chain: trace_path.clone(),
1277 payload: LogPayload::Sql(sql_log_entry.clone()),
1278 });
1279 }
1280 }
1281
1282 crate::log_formatter::LogManager::write_sql_log(&trace_path, &sql_log_entry);
1283 }
1284
1285 pub(crate) fn record_metadata_log(&self, metadata: &teaql_data_service::ExecutionMetadata) {
1286 let operation = match metadata.operation {
1287 teaql_data_service::DataServiceOperation::Query => SqlLogOperation::Select,
1288 teaql_data_service::DataServiceOperation::Insert => SqlLogOperation::Insert,
1289 teaql_data_service::DataServiceOperation::Update => SqlLogOperation::Update,
1290 teaql_data_service::DataServiceOperation::Delete => SqlLogOperation::Delete,
1291 teaql_data_service::DataServiceOperation::Recover => SqlLogOperation::Update,
1292 teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
1293 teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
1294 };
1295 if !self.sql_log_options.enabled_for(operation) {
1296 return;
1297 }
1298 if let Some(debug_sql) = &metadata.debug_query {
1299 let trace_path =
1300 canonical_sql_trace_path(operation, &metadata.backend, &metadata.trace_chain);
1301 let sql_log_entry = SqlLogEntry {
1302 operation,
1303 comment: trace_value(&metadata.trace_chain, teaql_core::TraceKind::Comment)
1304 .or_else(|| metadata.comment.clone()),
1305 purpose: trace_value(&metadata.trace_chain, teaql_core::TraceKind::Purpose),
1306 audit_reason: trace_value(
1307 &metadata.trace_chain,
1308 teaql_core::TraceKind::AuditReason,
1309 ),
1310 trace_path: trace_path.clone(),
1311 sql: metadata.parameterized_query.clone().unwrap_or_default(),
1312 params: metadata.params.clone(),
1313 pretty_sql: pretty_sql(debug_sql),
1314 debug_sql: debug_sql.clone(),
1315 started_at: metadata.started_at,
1316 ended_at: metadata.ended_at,
1317 elapsed: metadata
1318 .ended_at
1319 .duration_since(metadata.started_at)
1320 .unwrap_or_default(),
1321 result_count: metadata.result_count,
1322 result_type: None, affected_rows: metadata.affected_rows,
1324 result_summary: String::new(), };
1326
1327 let mut summary = String::new();
1329 if let Some(c) = metadata.result_count {
1330 summary = format!("{} rows returned", c);
1331 } else if let Some(a) = metadata.affected_rows {
1332 summary = format!("{} rows affected", a);
1333 }
1334
1335 let mut final_entry = sql_log_entry;
1336 final_entry.result_summary = summary;
1337
1338 if let Ok(mut entries) = self.sql_log_entries.lock() {
1339 entries.push(final_entry.clone());
1340 }
1341
1342 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
1343 if let Ok(mut entries) = buf.entries.lock() {
1344 entries.push(UnifiedLogEntry {
1345 timestamp: metadata.started_at,
1346 user_identifier: self.user_identifier.clone(),
1347 trace_chain: trace_path.clone(),
1348 payload: LogPayload::Sql(final_entry.clone()),
1349 });
1350 }
1351 }
1352
1353 crate::log_formatter::LogManager::write_sql_log(&trace_path, &final_entry);
1354 }
1355 }
1356
1357 pub fn language(&self) -> Language {
1358 self.language
1359 }
1360
1361 pub fn set_language_code(&mut self, code: &str) -> Result<(), RuntimeError> {
1362 let Some(language) = Language::from_code(code) else {
1363 return Err(RuntimeError::UnsupportedLocale(code.to_owned()));
1364 };
1365 self.language = language;
1366 Ok(())
1367 }
1368
1369 pub fn set_locale_code(&mut self, code: &str) -> Result<(), RuntimeError> {
1370 self.set_language_code(code)
1371 }
1372
1373 pub fn generate_id(&self, entity: &str) -> Result<Option<u64>, RuntimeError> {
1374 self.internal_id_generator
1375 .as_ref()
1376 .map(|generator| generator.generate_id(entity))
1377 .transpose()
1378 }
1379
1380 pub fn next_id(&self, entity: &str) -> Result<u64, RuntimeError> {
1381 match self.generate_id(entity)? {
1382 Some(id) => Ok(id),
1383 None => local_id_generator().generate_id(entity),
1384 }
1385 }
1386
1387 pub fn entity(&self, name: &str) -> Option<&EntityDescriptor> {
1388 self.metadata
1389 .as_ref()
1390 .and_then(|metadata| metadata.entity(name))
1391 }
1392
1393 pub fn all_entities(&self) -> Vec<&EntityDescriptor> {
1394 self.metadata
1395 .as_ref()
1396 .map(|metadata| metadata.all_entities())
1397 .unwrap_or_default()
1398 }
1399
1400 pub fn require_entity(&self, name: &str) -> Result<&EntityDescriptor, RuntimeError> {
1401 self.entity(name)
1402 .ok_or_else(|| RuntimeError::MissingEntity(name.to_owned()))
1403 }
1404
1405 pub fn insert_resource<T>(&mut self, resource: T)
1406 where
1407 T: Send + Sync + 'static,
1408 {
1409 self.typed_resources
1410 .insert(TypeId::of::<T>(), Box::new(resource));
1411 }
1412
1413 pub fn get_resource<T>(&self) -> Option<&T>
1414 where
1415 T: Send + Sync + 'static,
1416 {
1417 self.typed_resources
1418 .get(&TypeId::of::<T>())
1419 .and_then(|value| value.downcast_ref::<T>())
1420 }
1421
1422 pub fn require_resource<T>(&self) -> Result<&T, ContextError>
1423 where
1424 T: Send + Sync + 'static,
1425 {
1426 self.get_resource::<T>()
1427 .ok_or(ContextError::MissingTypedResource(
1428 std::any::type_name::<T>(),
1429 ))
1430 }
1431
1432 pub fn insert_named_resource<T>(&mut self, name: impl Into<String>, resource: T)
1433 where
1434 T: Send + Sync + 'static,
1435 {
1436 self.named_resources.insert(name.into(), Box::new(resource));
1437 }
1438
1439 pub fn get_named_resource<T>(&self, name: &str) -> Option<&T>
1440 where
1441 T: Send + Sync + 'static,
1442 {
1443 self.named_resources
1444 .get(name)
1445 .and_then(|value| value.downcast_ref::<T>())
1446 }
1447
1448 pub fn require_named_resource<T>(&self, name: &str) -> Result<&T, ContextError>
1449 where
1450 T: Send + Sync + 'static,
1451 {
1452 self.get_named_resource::<T>(name)
1453 .ok_or_else(|| ContextError::MissingResource(name.to_owned()))
1454 }
1455
1456 pub fn put_local(&mut self, key: impl Into<String>, value: impl Into<Value>) {
1457 self.locals.insert(key.into(), value.into());
1458 }
1459
1460 pub fn local(&self, key: &str) -> Option<&Value> {
1461 self.locals.get(key)
1462 }
1463
1464 pub fn remove_local(&mut self, key: &str) -> Option<Value> {
1465 self.locals.remove(key)
1466 }
1467
1468 pub fn has_entity_data_service(&self, entity: &str) -> bool {
1469 let in_registry = self
1470 .entity_registry
1471 .as_ref()
1472 .map(|registry| registry.contains(entity))
1473 .unwrap_or(false);
1474 in_registry || self.entity(entity).is_some()
1475 }
1476
1477 pub fn entity_data_service_behavior(
1478 &self,
1479 entity: &str,
1480 ) -> Option<std::sync::Arc<dyn EntityDataServiceBehavior>> {
1481 self.entity_data_service_behavior_registry
1482 .as_ref()
1483 .and_then(|registry| registry.behavior(entity))
1484 }
1485
1486 pub fn has_checker(&self, entity: &str) -> bool {
1487 self.checker_registry
1488 .as_ref()
1489 .and_then(|registry| registry.checker(entity))
1490 .is_some()
1491 }
1492
1493 pub fn fix_time(&self) -> teaql_core::time::Timestamp {
1497 crate::entity_save::current_graph_fix_time()
1498 }
1499
1500 pub fn record_fix_evidence(&self, evidence: FixEvidence) {
1501 crate::entity_save::record_graph_fix_evidence(evidence);
1502 }
1503
1504 pub(crate) fn replace_last_fix_evidence(&self, evidence: Vec<FixEvidence>) {
1505 *self.last_fix_evidence.lock().unwrap() = evidence;
1506 }
1507
1508 pub fn last_fix_evidence(&self) -> Vec<FixEvidence> {
1509 self.last_fix_evidence.lock().unwrap().clone()
1510 }
1511
1512 pub fn check_and_fix_values(
1513 &self,
1514 entity: &str,
1515 values: &mut crate::EntityValues,
1516 ) -> Result<(), RuntimeError> {
1517 self.check_and_fix_values_at(entity, values, &ObjectLocation::root())
1518 }
1519
1520 pub fn check_and_fix_values_at(
1521 &self,
1522 entity: &str,
1523 values: &mut crate::EntityValues,
1524 location: &ObjectLocation,
1525 ) -> Result<(), RuntimeError> {
1526 let status = CheckObjectStatus::from_values(values);
1527 let checker = self
1528 .checker_registry
1529 .as_ref()
1530 .and_then(|registry| registry.checker(entity));
1531 let mut results = CheckResults::new();
1532 if let Some(checker) = checker {
1533 checker.check_and_fix(self, values, location, &mut results);
1534 }
1535
1536 if let Some(descriptor) = self
1541 .metadata
1542 .as_ref()
1543 .and_then(|metadata| metadata.entity(entity))
1544 {
1545 for property in descriptor
1546 .properties
1547 .iter()
1548 .filter(|property| !property.nullable && !property.is_version)
1552 {
1553 let missing = !values.contains_key(&property.name);
1554 let null = matches!(values.get(&property.name), Some(Value::Null));
1555 let property_location = location.clone().member(&property.name);
1556 let already_reported = results.iter().any(|result| {
1557 result.rule == crate::CheckRule::Required
1558 && result.location == property_location
1559 });
1560 if ((status.is_create() && missing) || null) && !already_reported {
1561 results.push(CheckResult::required(property_location));
1562 }
1563 }
1564 }
1565 if results.is_empty() {
1566 return Ok(());
1567 }
1568 self.translate_check_results(&mut results);
1569 Err(RuntimeError::Check(results))
1570 }
1571
1572 pub fn translate_check_results(&self, results: &mut CheckResults) {
1573 for result in results {
1574 if result.message.is_none() {
1575 result.message = Some(
1576 self.i18n_catalog
1577 .translate_check_result(self.language, result),
1578 );
1579 }
1580 }
1581 }
1582
1583 pub fn send_event(&self, mut event: RawAuditEvent) -> Result<(), RuntimeError> {
1584 if self.is_generated_schema_bootstrap()
1585 && matches!(
1586 event.kind,
1587 crate::RawAuditEventKind::Created | crate::RawAuditEventKind::Updated
1588 )
1589 {
1590 let reason = event
1591 .trace_chain
1592 .last()
1593 .map(|node| node.comment.clone())
1594 .unwrap_or_else(|| "generated runtime bootstrap".to_owned());
1595 let resulting_version = event
1596 .new_values
1597 .as_ref()
1598 .and_then(|values| values.get("version"))
1599 .or_else(|| event.values.get("version"))
1600 .and_then(teaql_core::Value::try_i64);
1601 let occurred_at_millis = std::time::SystemTime::now()
1602 .duration_since(std::time::UNIX_EPOCH)
1603 .unwrap_or_default()
1604 .as_millis() as u64;
1605 event.bootstrap_audit = Some(crate::BootstrapAuditIdentity {
1606 actor: "teaql-generated-bootstrap".to_owned(),
1607 category: "runtime-bootstrap".to_owned(),
1608 reason,
1609 resulting_version,
1610 occurred_at_millis,
1611 });
1612 }
1613 let scope = self.start_runtime_operation(
1614 crate::RuntimeOperation::new("audit", format!("{}.event", event.entity))
1615 .attribute("teaql.entity.type", event.entity.clone()),
1616 );
1617 let result = self.send_event_inner(event);
1618 match &result {
1619 Ok(()) => scope.success(std::collections::BTreeMap::new()),
1620 Err(_) => scope.failure("audit_error"),
1621 }
1622 result
1623 }
1624
1625 fn send_event_inner(&self, event: RawAuditEvent) -> Result<(), RuntimeError> {
1626 if let Some(sink) = self.event_sink.as_ref() {
1627 sink.on_event(self, &event)?;
1628 }
1629 if let Some(sink) = self.custom_event_sink.as_ref() {
1630 let (mask_fields, max_len) = self
1631 .metadata
1632 .as_ref()
1633 .and_then(|metadata| metadata.entity(&event.entity))
1634 .map(|desc| (desc.audit_mask_fields.clone(), desc.audit_value_max_len))
1635 .unwrap_or_else(|| (vec![], None));
1636
1637 let safe_event = event.build_safe_event(&mask_fields, max_len);
1638 sink.on_safe_event(self, &safe_event)?;
1639 }
1640
1641 crate::log_formatter::LogManager::write_audit_log(&event);
1642
1643 Ok(())
1644 }
1645
1646 pub async fn get_in_store(&self, key: &str) -> Option<Value> {
1647 let store = self.get_resource::<Box<dyn DataStore>>()?;
1648 store.get(key).await
1649 }
1650
1651 pub async fn put_in_store(
1652 &self,
1653 key: &str,
1654 value: impl Into<Value>,
1655 timeout_seconds: Option<u64>,
1656 ) {
1657 if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1658 store.put(key, value.into(), timeout_seconds).await;
1659 }
1660 }
1661
1662 pub async fn clear_in_store(&self, key: &str) {
1663 if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1664 store.remove(key).await;
1665 }
1666 }
1667}
1668
1669fn extract_id_from_sql(sql: &str) -> Option<String> {
1670 let sql_lower = sql.to_lowercase();
1671 let where_idx = sql_lower.find("where")?;
1672 let where_clause = &sql_lower[where_idx + 5..];
1673
1674 let bytes = where_clause.as_bytes();
1675 let mut i = 0;
1676 while i < bytes.len() {
1677 if i + 1 < bytes.len() && &bytes[i..i + 2] == b"id" {
1678 let prev_ok = i == 0 || {
1680 let prev_char = bytes[i - 1] as char;
1681 !prev_char.is_ascii_alphanumeric() && prev_char != '_' && prev_char != '.'
1682 };
1683 let next_ok = i + 2 == bytes.len() || {
1685 let next_char = bytes[i + 2] as char;
1686 !next_char.is_ascii_alphanumeric() && next_char != '_'
1687 };
1688
1689 if prev_ok && next_ok {
1690 let mut j = i + 2;
1693 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1694 j += 1;
1695 }
1696 if j < bytes.len() && bytes[j] == b'=' {
1697 j += 1;
1698 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1699 j += 1;
1700 }
1701 let mut val_str = String::new();
1703 if j < bytes.len() && bytes[j] == b'\'' {
1704 j += 1; while j < bytes.len() && bytes[j] != b'\'' {
1706 val_str.push(bytes[j] as char);
1707 j += 1;
1708 }
1709 return Some(val_str);
1710 }
1711 while j < bytes.len() {
1713 let c = bytes[j] as char;
1714 if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
1715 break;
1716 }
1717 val_str.push(c);
1718 j += 1;
1719 }
1720 if !val_str.is_empty() {
1721 return Some(val_str);
1722 }
1723 }
1724 }
1725 }
1726 i += 1;
1727 }
1728 None
1729}
1730
1731fn sql_result_summary(
1732 operation: SqlLogOperation,
1733 result_count: Option<usize>,
1734 result_type: Option<&str>,
1735 affected_rows: Option<u64>,
1736 debug_sql: &str,
1737) -> String {
1738 match operation {
1739 SqlLogOperation::Select => {
1740 let count = result_count.unwrap_or(0);
1741 match count {
1742 0 => "MISS".to_owned(),
1743 1 => match result_type {
1744 Some(result_type) => extract_id_from_sql(debug_sql)
1745 .map(|id| format!("{result_type}({id})"))
1746 .unwrap_or_else(|| result_type.to_owned()),
1747 None => "row".to_owned(),
1748 },
1749 _ => match result_type {
1750 Some(result_type) => format!("{count}*{result_type}"),
1751 None => format!("{count}*rows"),
1752 },
1753 }
1754 }
1755 _ => {
1756 let affected = affected_rows.unwrap_or(0);
1757 format!("{affected} UPDATED")
1758 }
1759 }
1760}
1761
1762fn trace_value(
1763 trace_path: &[teaql_core::TraceNode],
1764 kind: teaql_core::TraceKind,
1765) -> Option<String> {
1766 trace_path
1767 .iter()
1768 .rev()
1769 .find(|node| node.kind == kind)
1770 .map(|node| node.comment.clone())
1771}
1772
1773fn canonical_sql_trace_path(
1774 operation: SqlLogOperation,
1775 backend: &str,
1776 source: &[teaql_core::TraceNode],
1777) -> Vec<teaql_core::TraceNode> {
1778 use teaql_core::{TraceKind, TraceNode};
1779
1780 if source.iter().any(|node| node.kind == TraceKind::Operation)
1781 && source.iter().any(|node| node.kind == TraceKind::Provider)
1782 && source.iter().any(|node| node.kind == TraceKind::Sql)
1783 {
1784 return source
1785 .iter()
1786 .filter(|node| {
1787 !matches!(
1788 node.kind,
1789 TraceKind::Comment | TraceKind::Purpose | TraceKind::AuditReason
1790 )
1791 })
1792 .cloned()
1793 .collect();
1794 }
1795
1796 let entity = source
1797 .iter()
1798 .find(|node| !node.entity_type.trim().is_empty())
1799 .map(|node| node.entity_type.clone())
1800 .unwrap_or_else(|| "unknown".to_owned());
1801 let family = if operation.is_select() {
1802 "query"
1803 } else {
1804 "mutation"
1805 };
1806 let statement = match operation {
1807 SqlLogOperation::Select => "select",
1808 SqlLogOperation::Insert => "insert",
1809 SqlLogOperation::Update => "update",
1810 SqlLogOperation::Delete => "delete",
1811 SqlLogOperation::Recover => "recover",
1812 };
1813 let mut path = vec![TraceNode::typed(TraceKind::Operation, entity.clone(), None, family)];
1814 path.push(TraceNode::typed(
1815 if operation.is_select() {
1816 TraceKind::Request
1817 } else {
1818 TraceKind::Entity
1819 },
1820 entity,
1821 None,
1822 "",
1823 ));
1824 path.extend(
1825 source
1826 .iter()
1827 .filter(|node| node.kind == TraceKind::Relation)
1828 .cloned(),
1829 );
1830 path.push(TraceNode::typed(
1831 TraceKind::Provider,
1832 if backend.trim().is_empty() {
1833 "unknown"
1834 } else {
1835 backend
1836 },
1837 None,
1838 "",
1839 ));
1840 path.push(TraceNode::typed(TraceKind::Sql, statement, None, ""));
1841 path
1842}
1843
1844fn pretty_sql(sql: &str) -> String {
1845 let mut pretty = sql.to_owned();
1846 for keyword in [
1847 " FROM ",
1848 " WHERE ",
1849 " GROUP BY ",
1850 " HAVING ",
1851 " ORDER BY ",
1852 " LIMIT ",
1853 " OFFSET ",
1854 " RETURNING ",
1855 ] {
1856 pretty = pretty.replace(keyword, &format!("\n{}", keyword.trim_start()));
1857 }
1858 pretty.replace(" AND ", "\n AND ")
1859}
1860
1861#[cfg(test)]
1862mod sql_log_option_tests {
1863 use super::*;
1864
1865 #[test]
1866 fn diagnostic_sql_log_is_enabled_by_default_with_independent_switches() {
1867 let mut context = UserContext::default();
1868 assert_eq!(context.sql_log_options(), SqlLogOptions::all());
1869 assert!(context.sql_logs().is_empty());
1870
1871 context.disable_select_sql_log();
1872 assert_eq!(context.sql_log_options(), SqlLogOptions::mutation_only());
1873
1874 context.enable_select_sql_log();
1875 context.disable_mutation_sql_log();
1876 assert_eq!(context.sql_log_options(), SqlLogOptions::select_only());
1877
1878 context.disable_sql_log();
1879 assert_eq!(context.sql_log_options(), SqlLogOptions::disabled());
1880 }
1881
1882 #[test]
1883 fn disabled_sql_log_rejects_executor_metadata_before_recording() {
1884 let mut context = UserContext::default();
1885 context.disable_sql_log();
1886 let now = SystemTime::now();
1887 context.record_metadata_log(&teaql_data_service::ExecutionMetadata {
1888 backend: "sql".to_owned(),
1889 operation: teaql_data_service::DataServiceOperation::Query,
1890 started_at: now,
1891 ended_at: now,
1892 affected_rows: None,
1893 result_count: Some(1),
1894 trace_chain: Vec::new(),
1895 comment: Some("disabled log test".to_owned()),
1896 backend_request_id: None,
1897 parameterized_query: Some("SELECT id FROM sample WHERE id = $1".to_owned()),
1898 params: vec![Value::I64(1)],
1899 debug_query: Some("SELECT id FROM sample WHERE id = 1".to_owned()),
1900 });
1901 assert!(context.sql_logs().is_empty());
1902 }
1903
1904 #[test]
1905 fn metadata_log_retains_structured_intent_sql_forms_and_multilevel_trace() {
1906 let context = UserContext::default();
1907 let now = SystemTime::now();
1908 let query_trace = vec![
1909 teaql_core::TraceNode::typed(
1910 teaql_core::TraceKind::Comment,
1911 "School",
1912 None,
1913 "what: load school graph",
1914 ),
1915 teaql_core::TraceNode::typed(
1916 teaql_core::TraceKind::Purpose,
1917 "School",
1918 None,
1919 "why: render school details",
1920 ),
1921 teaql_core::TraceNode::typed(
1922 teaql_core::TraceKind::Relation,
1923 "platform",
1924 None,
1925 "School.platform",
1926 ),
1927 teaql_core::TraceNode::typed(
1928 teaql_core::TraceKind::Relation,
1929 "organization",
1930 None,
1931 "Platform.organization",
1932 ),
1933 teaql_core::TraceNode::typed(
1934 teaql_core::TraceKind::Relation,
1935 "region",
1936 None,
1937 "Organization.region",
1938 ),
1939 ];
1940 context.record_metadata_log(&teaql_data_service::ExecutionMetadata {
1941 backend: "sqlite".to_owned(),
1942 operation: teaql_data_service::DataServiceOperation::Query,
1943 started_at: now,
1944 ended_at: now,
1945 affected_rows: None,
1946 result_count: Some(2),
1947 trace_chain: query_trace.clone(),
1948 comment: None,
1949 backend_request_id: None,
1950 parameterized_query: Some("SELECT name FROM school_data WHERE id = ?".to_owned()),
1951 params: vec![Value::I64(7)],
1952 debug_query: Some("SELECT name FROM school_data WHERE id = 7".to_owned()),
1953 });
1954
1955 let query = context.sql_logs().pop().expect("query log");
1956 assert_eq!(query.comment.as_deref(), Some("what: load school graph"));
1957 assert_eq!(query.purpose.as_deref(), Some("why: render school details"));
1958 assert_eq!(query.audit_reason, None);
1959 assert_eq!(
1960 query
1961 .trace_path
1962 .iter()
1963 .map(|node| node.kind)
1964 .collect::<Vec<_>>(),
1965 vec![
1966 teaql_core::TraceKind::Operation,
1967 teaql_core::TraceKind::Request,
1968 teaql_core::TraceKind::Relation,
1969 teaql_core::TraceKind::Relation,
1970 teaql_core::TraceKind::Relation,
1971 teaql_core::TraceKind::Provider,
1972 teaql_core::TraceKind::Sql,
1973 ]
1974 );
1975 assert_eq!(query.trace_path[0].entity_type, "School");
1976 assert_eq!(query.trace_path[5].entity_type, "sqlite");
1977 assert_eq!(query.trace_path[6].entity_type, "select");
1978 assert_eq!(query.sql, "SELECT name FROM school_data WHERE id = ?");
1979 assert_eq!(query.params, vec![Value::I64(7)]);
1980 assert_eq!(query.debug_sql, "SELECT name FROM school_data WHERE id = 7");
1981 assert_eq!(query.result_count, Some(2));
1982
1983 let mutation_trace = vec![teaql_core::TraceNode::typed(
1984 teaql_core::TraceKind::AuditReason,
1985 "School",
1986 Some(7),
1987 "correct school name",
1988 )];
1989 context.record_metadata_log(&teaql_data_service::ExecutionMetadata {
1990 backend: "sqlite".to_owned(),
1991 operation: teaql_data_service::DataServiceOperation::Update,
1992 started_at: now,
1993 ended_at: now,
1994 affected_rows: Some(1),
1995 result_count: None,
1996 trace_chain: mutation_trace.clone(),
1997 comment: None,
1998 backend_request_id: None,
1999 parameterized_query: Some("UPDATE school_data SET name = ? WHERE id = ?".to_owned()),
2000 params: vec![Value::from("Academy"), Value::I64(7)],
2001 debug_query: Some("UPDATE school_data SET name = 'Academy' WHERE id = 7".to_owned()),
2002 });
2003 let mutation = context.sql_logs().pop().expect("mutation log");
2004 assert_eq!(mutation.comment, None);
2005 assert_eq!(mutation.purpose, None);
2006 assert_eq!(
2007 mutation.audit_reason.as_deref(),
2008 Some("correct school name")
2009 );
2010 assert_eq!(
2011 mutation
2012 .trace_path
2013 .iter()
2014 .map(|node| node.kind)
2015 .collect::<Vec<_>>(),
2016 vec![
2017 teaql_core::TraceKind::Operation,
2018 teaql_core::TraceKind::Entity,
2019 teaql_core::TraceKind::Provider,
2020 teaql_core::TraceKind::Sql,
2021 ]
2022 );
2023 assert_eq!(mutation.affected_rows, Some(1));
2024 }
2025}
2026
2027#[cfg(test)]
2028mod entity_runtime_state_tests {
2029 use super::*;
2030 use crate::EntityKey;
2031
2032 #[test]
2033 fn reused_user_context_returns_independent_mutation_ledgers() {
2034 let context = UserContext::default();
2035 let first = context.entity_runtime_state();
2036 let key = EntityKey::new("School", 1_u64);
2037 first.set(key.clone(), "name", "First");
2038
2039 let second = context.entity_runtime_state();
2040
2041 assert_eq!(first.changed_field_names(&key).len(), 1);
2042 assert!(second.changed_field_names(&key).is_empty());
2043 }
2044}