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