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::{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 ctx: &'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}
237
238#[derive(Clone, Copy)]
239struct LocalLockEntry {
240 owner: u64,
241 expires_at: Option<Instant>,
242}
243
244#[derive(Default)]
245struct ProcessLocalLocks {
246 entries: Mutex<HashMap<String, LocalLockEntry>>,
247 changed: Condvar,
248}
249
250static PROCESS_LOCAL_LOCKS: OnceLock<ProcessLocalLocks> = OnceLock::new();
251static NEXT_LOCAL_LOCK_OWNER: AtomicU64 = AtomicU64::new(1);
252
253impl Default for UserContext {
254 fn default() -> Self {
255 let pid = std::process::id();
256 let thread_id_str = format!("{:?}", std::thread::current().id());
257 let numeric_thread_id = thread_id_str
258 .strip_prefix("ThreadId(")
259 .and_then(|s| s.strip_suffix(")"))
260 .unwrap_or(&thread_id_str);
261 let os_user = std::env::var("USER")
262 .or_else(|_| std::env::var("USERNAME"))
263 .unwrap_or_else(|_| "main".to_owned());
264 let user_id = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
265 Self {
266 metadata: None,
267 entity_registry: None,
268 entity_data_service_behavior_registry: None,
269 request_policy: None,
270 checker_registry: None,
271 event_sink: None,
272 custom_event_sink: None,
273 internal_id_generator: None,
274 schema_provider: None,
275 language: Language::default(),
276 typed_resources: HashMap::new(),
277 named_resources: BTreeMap::new(),
278 locals: BTreeMap::new(),
279 initial_graphs: Vec::new(),
280 entity_root: EntityRoot::default(),
281 sql_log_options: SqlLogOptions::all(),
282 sql_log_entries: Mutex::new(Vec::new()),
283 user_identifier: Some(user_id),
284 timezone: Some("UTC".to_owned()),
285 trace_id: format!(
286 "req-{pid}-{numeric_thread_id}-{:x}",
287 std::time::SystemTime::now()
288 .duration_since(std::time::UNIX_EPOCH)
289 .unwrap_or_default()
290 .as_micros()
291 ),
292 continuous_page_cursor_store: std::sync::Arc::new(
293 InMemoryContinuousPageCursorStore::default(),
294 ),
295 continuous_page_observation: Mutex::new(("DISABLED".to_owned(), None)),
296 local_lock_owner: NEXT_LOCAL_LOCK_OWNER.fetch_add(1, Ordering::Relaxed),
297 }
298 }
299}
300
301#[async_trait::async_trait]
302pub trait DataStore: Send + Sync + 'static {
303 async fn get(&self, key: &str) -> Option<Value>;
304 async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>);
305 async fn remove(&self, key: &str);
306}
307
308#[derive(Default)]
309pub struct InMemoryDataStore {
310 cache: std::sync::RwLock<HashMap<String, (Value, Option<std::time::Instant>)>>,
311}
312
313#[async_trait::async_trait]
314impl DataStore for InMemoryDataStore {
315 async fn get(&self, key: &str) -> Option<Value> {
316 let lock = self.cache.read().unwrap();
317 if let Some((val, expires_at)) = lock.get(key) {
318 if let Some(exp) = expires_at {
319 if std::time::Instant::now() > *exp {
320 return None;
321 }
322 }
323 return Some(val.clone());
324 }
325 None
326 }
327
328 async fn put(&self, key: &str, value: Value, timeout_seconds: Option<u64>) {
329 let mut lock = self.cache.write().unwrap();
330 let expires_at = timeout_seconds
331 .map(|secs| std::time::Instant::now() + std::time::Duration::from_secs(secs));
332 lock.insert(key.to_string(), (value, expires_at));
333 }
334
335 async fn remove(&self, key: &str) {
336 let mut lock = self.cache.write().unwrap();
337 lock.remove(key);
338 }
339}
340
341impl UserContext {
342 pub fn new() -> Self {
343 Self::default()
344 }
345
346 pub fn try_local_lock(&self, key: &str, timeout_millis: u64, expire_millis: u64) -> bool {
347 let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
348 let deadline = Instant::now() + Duration::from_millis(timeout_millis);
349 let mut entries = locks.entries.lock().expect("local lock state poisoned");
350 loop {
351 let now = Instant::now();
352 match entries.get(key).copied() {
353 None => {
354 entries.insert(key.to_owned(), LocalLockEntry {
355 owner: self.local_lock_owner,
356 expires_at: (expire_millis > 0)
357 .then(|| now + Duration::from_millis(expire_millis)),
358 });
359 return true;
360 }
361 Some(current)
362 if current.owner == self.local_lock_owner
363 || current.expires_at.is_some_and(|expiry| now >= expiry) =>
364 {
365 entries.insert(key.to_owned(), LocalLockEntry {
366 owner: self.local_lock_owner,
367 expires_at: (expire_millis > 0)
368 .then(|| now + Duration::from_millis(expire_millis)),
369 });
370 return true;
371 }
372 Some(current) => {
373 if timeout_millis == 0 || now >= deadline { return false; }
374 let wake_after = current.expires_at
375 .map(|expiry| expiry.saturating_duration_since(now))
376 .unwrap_or_else(|| deadline.saturating_duration_since(now))
377 .min(deadline.saturating_duration_since(now));
378 let waited = locks.changed.wait_timeout(entries, wake_after)
379 .expect("local lock state poisoned");
380 entries = waited.0;
381 }
382 }
383 }
384 }
385
386 pub fn unlock_local(&self, key: &str) {
387 let locks = PROCESS_LOCAL_LOCKS.get_or_init(ProcessLocalLocks::default);
388 let mut entries = locks.entries.lock().expect("local lock state poisoned");
389 if entries.get(key).is_some_and(|entry| entry.owner == self.local_lock_owner) {
390 entries.remove(key);
391 locks.changed.notify_all();
392 }
393 }
394
395 pub fn user_identifier(&self) -> Option<&str> {
396 self.user_identifier.as_deref()
397 }
398
399 pub fn set_user_identifier(&mut self, user_identifier: impl Into<String>) {
400 self.user_identifier = Some(user_identifier.into());
401 }
402
403 pub fn set_continuous_page_cursor_store(
404 &mut self,
405 store: std::sync::Arc<dyn ContinuousPageCursorStore>,
406 ) {
407 self.continuous_page_cursor_store = store;
408 }
409
410 pub fn continuous_page_plan(&self) -> Option<String> {
411 self.continuous_page_observation
412 .lock()
413 .ok()
414 .map(|value| value.0.clone())
415 }
416
417 pub fn continuous_page_cursor_id(&self) -> Option<String> {
418 self.continuous_page_observation
419 .lock()
420 .ok()
421 .and_then(|value| value.1.clone())
422 }
423
424 pub(crate) fn observe_continuous_page(
425 &self,
426 plan: impl Into<String>,
427 cursor_id: Option<String>,
428 ) {
429 if let Ok(mut observation) = self.continuous_page_observation.lock() {
430 *observation = (plan.into(), cursor_id);
431 }
432 }
433
434 pub(crate) fn continuous_page_cursor_store(&self) -> &dyn ContinuousPageCursorStore {
435 self.continuous_page_cursor_store.as_ref()
436 }
437
438 pub fn with_user_identifier(mut self, user_identifier: impl Into<String>) -> Self {
439 self.user_identifier = Some(user_identifier.into());
440 self
441 }
442
443 pub fn set_user_identifier_option(&mut self, user_identifier: Option<String>) {
444 self.user_identifier = user_identifier;
445 }
446
447 pub fn with_user_identifier_option(mut self, user_identifier: Option<String>) -> Self {
448 self.user_identifier = user_identifier;
449 self
450 }
451
452 pub fn timezone(&self) -> Option<&str> {
453 self.timezone.as_deref()
454 }
455
456 pub fn set_timezone(&mut self, timezone: impl Into<String>) {
457 self.timezone = Some(timezone.into());
458 }
459
460 pub fn with_timezone(mut self, timezone: impl Into<String>) -> Self {
461 self.timezone = Some(timezone.into());
462 self
463 }
464
465 pub fn trace_id(&self) -> &str {
466 &self.trace_id
467 }
468
469 pub fn set_trace_id(&mut self, trace_id: impl Into<String>) {
470 self.trace_id = trace_id.into();
471 }
472
473 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
474 self.trace_id = trace_id.into();
475 self
476 }
477
478 pub fn with_module(mut self, module: crate::RuntimeModule) -> Self {
479 module.apply_to(&mut self);
480 self
481 }
482
483 pub fn entity_root(&self) -> EntityRoot {
484 self.entity_root.clone()
485 }
486
487 pub fn initial_graphs(&self) -> &[GraphNode] {
488 &self.initial_graphs
489 }
490
491 pub fn set_initial_graphs(&mut self, graphs: Vec<GraphNode>) {
492 self.initial_graphs = graphs;
493 }
494
495 pub fn with_metadata(mut self, metadata: impl MetadataStore + 'static) -> Self {
496 self.metadata = Some(Box::new(metadata));
497 self
498 }
499
500 pub fn set_metadata(&mut self, metadata: impl MetadataStore + 'static) {
501 self.metadata = Some(Box::new(metadata));
502 }
503
504 pub fn with_entity_registry(mut self, registry: impl EntityRegistry + 'static) -> Self {
505 self.entity_registry = Some(Box::new(registry));
506 self
507 }
508
509 pub fn set_entity_registry(&mut self, registry: impl EntityRegistry + 'static) {
510 self.entity_registry = Some(Box::new(registry));
511 }
512
513 pub fn with_entity_data_service_behavior_registry(
514 mut self,
515 registry: impl EntityDataServiceBehaviorRegistry + 'static,
516 ) -> Self {
517 self.entity_data_service_behavior_registry = Some(Box::new(registry));
518 self
519 }
520
521 pub fn set_entity_data_service_behavior_registry(
522 &mut self,
523 registry: impl EntityDataServiceBehaviorRegistry + 'static,
524 ) {
525 self.entity_data_service_behavior_registry = Some(Box::new(registry));
526 }
527
528 pub fn with_request_policy(mut self, policy: impl RequestPolicy + 'static) -> Self {
529 self.request_policy = Some(Box::new(policy));
530 self
531 }
532
533 pub fn set_request_policy(&mut self, policy: impl RequestPolicy + 'static) {
534 self.request_policy = Some(Box::new(policy));
535 }
536
537 pub fn clear_request_policy(&mut self) {
538 self.request_policy = None;
539 }
540
541 pub fn with_checker_registry(mut self, registry: impl CheckerRegistry + 'static) -> Self {
542 self.checker_registry = Some(Box::new(registry));
543 self
544 }
545
546 pub fn set_checker_registry(&mut self, registry: impl CheckerRegistry + 'static) {
547 self.checker_registry = Some(Box::new(registry));
548 }
549
550 pub(crate) fn with_event_sink(mut self, sink: impl RawAuditEventSink + 'static) -> Self {
551 self.event_sink = Some(Box::new(sink));
552 self
553 }
554
555 pub(crate) fn set_event_sink(&mut self, sink: impl RawAuditEventSink + 'static) {
556 self.event_sink = Some(Box::new(sink));
557 }
558
559 pub fn with_custom_event_sink(
560 mut self,
561 sink: impl crate::SafeAuditEventSink + 'static,
562 ) -> Self {
563 self.custom_event_sink = Some(Box::new(sink));
564 self
565 }
566
567 pub fn set_custom_event_sink(&mut self, sink: impl crate::SafeAuditEventSink + 'static) {
568 self.custom_event_sink = Some(Box::new(sink));
569 }
570
571 pub fn with_internal_id_generator(
572 mut self,
573 generator: impl InternalIdGenerator + 'static,
574 ) -> Self {
575 self.internal_id_generator = Some(Box::new(generator));
576 self
577 }
578
579 pub fn set_internal_id_generator(&mut self, generator: impl InternalIdGenerator + 'static) {
580 self.internal_id_generator = Some(Box::new(generator));
581 }
582
583 pub fn with_schema_provider(mut self, provider: impl SchemaProvider + 'static) -> Self {
584 self.schema_provider = Some(Box::new(provider));
585 self
586 }
587
588 pub fn set_schema_provider(&mut self, provider: impl SchemaProvider + 'static) {
589 self.schema_provider = Some(Box::new(provider));
590 }
591
592 pub async fn ensure_schema(&self) -> Result<(), RuntimeError> {
593 let provider = self
594 .schema_provider
595 .as_ref()
596 .ok_or_else(|| RuntimeError::Schema("missing schema provider".to_owned()))?;
597 provider.ensure_schema(self).await
598 }
599
600 pub fn with_language(mut self, language: Language) -> Self {
601 self.language = language;
602 self
603 }
604
605 pub fn set_language(&mut self, language: Language) {
606 self.language = language;
607 }
608
609 pub fn with_sql_log_options(mut self, options: SqlLogOptions) -> Self {
610 self.sql_log_options = options;
611 self
612 }
613
614 pub fn set_sql_log_options(&mut self, options: SqlLogOptions) {
615 self.sql_log_options = options;
616 }
617
618 pub fn enable_select_sql_log(&mut self) {
619 self.sql_log_options.select = true;
620 }
621
622 pub fn enable_mutation_sql_log(&mut self) {
623 self.sql_log_options.mutation = true;
624 }
625
626 pub fn enable_all_sql_log(&mut self) {
627 self.sql_log_options = SqlLogOptions::all();
628 }
629
630 pub fn disable_sql_log(&mut self) {
631 self.sql_log_options = SqlLogOptions::disabled();
632 self.clear_sql_logs();
633 }
634
635 pub fn sql_log_options(&self) -> SqlLogOptions {
636 self.sql_log_options
637 }
638
639 pub fn sql_logs(&self) -> Vec<SqlLogEntry> {
640 self.sql_log_entries
641 .lock()
642 .map(|entries| entries.clone())
643 .unwrap_or_default()
644 }
645
646 pub fn clear_sql_logs(&self) {
647 if let Ok(mut entries) = self.sql_log_entries.lock() {
648 entries.clear();
649 }
650 }
651
652 pub(crate) fn record_sql_log(
653 &self,
654 operation: SqlLogOperation,
655 query: &CompiledQuery,
656 database_kind: DatabaseKind,
657 started_at: SystemTime,
658 ended_at: SystemTime,
659 elapsed: Duration,
660 result_count: Option<usize>,
661 result_type: Option<String>,
662 affected_rows: Option<u64>,
663 trace_chain: Vec<teaql_core::TraceNode>,
664 ) {
665 if !self.sql_log_options.enabled_for(operation) {
666 return;
667 }
668 let debug_sql = query.debug_sql(database_kind);
669 let result_summary = sql_result_summary(
670 operation,
671 result_count,
672 result_type.as_deref(),
673 affected_rows,
674 &debug_sql,
675 );
676
677 let sql_log_entry = SqlLogEntry {
678 operation,
679 sql: query.sql.clone(),
680 params: query.params.clone(),
681 pretty_sql: pretty_sql(&debug_sql),
682 debug_sql: debug_sql.clone(),
683 started_at,
684 ended_at,
685 elapsed,
686 result_summary: result_summary.clone(),
687 result_count,
688 result_type,
689 affected_rows,
690 };
691
692 if let Ok(mut entries) = self.sql_log_entries.lock() {
693 entries.push(sql_log_entry.clone());
697 }
698
699 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
700 if let Ok(mut entries) = buf.entries.lock() {
701 entries.push(UnifiedLogEntry {
702 timestamp: started_at,
703 user_identifier: self.user_identifier.clone(),
704 trace_chain: trace_chain.clone(),
705 payload: LogPayload::Sql(sql_log_entry.clone()),
706 });
707 }
708 }
709
710 crate::log_formatter::LogManager::write_sql_log(&trace_chain, &sql_log_entry);
711 }
712
713 pub(crate) fn record_metadata_log(&self, metadata: &teaql_data_service::ExecutionMetadata) {
714 if let Some(debug_sql) = &metadata.debug_query {
715 let sql_log_entry = SqlLogEntry {
716 operation: match metadata.operation {
717 teaql_data_service::DataServiceOperation::Query => SqlLogOperation::Select,
718 teaql_data_service::DataServiceOperation::Insert => SqlLogOperation::Insert,
719 teaql_data_service::DataServiceOperation::Update => SqlLogOperation::Update,
720 teaql_data_service::DataServiceOperation::Delete => SqlLogOperation::Delete,
721 teaql_data_service::DataServiceOperation::Recover => SqlLogOperation::Update, teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
723 teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
724 },
725 sql: metadata.parameterized_query.clone().unwrap_or_default(),
726 params: metadata.params.clone(),
727 pretty_sql: pretty_sql(debug_sql),
728 debug_sql: debug_sql.clone(),
729 started_at: metadata.started_at,
730 ended_at: metadata.ended_at,
731 elapsed: metadata
732 .ended_at
733 .duration_since(metadata.started_at)
734 .unwrap_or_default(),
735 result_count: metadata.result_count,
736 result_type: None, affected_rows: metadata.affected_rows,
738 result_summary: String::new(), };
740
741 let mut summary = String::new();
743 if let Some(c) = metadata.result_count {
744 summary = format!("{} rows returned", c);
745 } else if let Some(a) = metadata.affected_rows {
746 summary = format!("{} rows affected", a);
747 }
748
749 let mut final_entry = sql_log_entry;
750 final_entry.result_summary = summary;
751
752 if let Ok(mut entries) = self.sql_log_entries.lock() {
753 entries.push(final_entry.clone());
754 }
755
756 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
757 if let Ok(mut entries) = buf.entries.lock() {
758 entries.push(UnifiedLogEntry {
759 timestamp: metadata.started_at,
760 user_identifier: self.user_identifier.clone(),
761 trace_chain: metadata.trace_chain.clone(),
762 payload: LogPayload::Sql(final_entry.clone()),
763 });
764 }
765 }
766
767 crate::log_formatter::LogManager::write_sql_log(&metadata.trace_chain, &final_entry);
768 }
769 }
770
771 pub fn language(&self) -> Language {
772 self.language
773 }
774
775 pub fn set_language_code(&mut self, code: &str) -> Result<(), RuntimeError> {
776 let Some(language) = Language::from_code(code) else {
777 return Err(RuntimeError::Language(format!(
778 "unsupported language code: {code}"
779 )));
780 };
781 self.language = language;
782 Ok(())
783 }
784
785 pub fn generate_id(&self, entity: &str) -> Result<Option<u64>, RuntimeError> {
786 self.internal_id_generator
787 .as_ref()
788 .map(|generator| generator.generate_id(entity))
789 .transpose()
790 }
791
792 pub fn next_id(&self, entity: &str) -> Result<u64, RuntimeError> {
793 match self.generate_id(entity)? {
794 Some(id) => Ok(id),
795 None => local_id_generator().generate_id(entity),
796 }
797 }
798
799 pub fn entity(&self, name: &str) -> Option<&EntityDescriptor> {
800 self.metadata
801 .as_ref()
802 .and_then(|metadata| metadata.entity(name))
803 }
804
805 pub fn all_entities(&self) -> Vec<&EntityDescriptor> {
806 self.metadata
807 .as_ref()
808 .map(|metadata| metadata.all_entities())
809 .unwrap_or_default()
810 }
811
812 pub fn require_entity(&self, name: &str) -> Result<&EntityDescriptor, RuntimeError> {
813 self.entity(name)
814 .ok_or_else(|| RuntimeError::MissingEntity(name.to_owned()))
815 }
816
817 pub fn insert_resource<T>(&mut self, resource: T)
818 where
819 T: Send + Sync + 'static,
820 {
821 self.typed_resources
822 .insert(TypeId::of::<T>(), Box::new(resource));
823 }
824
825 pub fn get_resource<T>(&self) -> Option<&T>
826 where
827 T: Send + Sync + 'static,
828 {
829 self.typed_resources
830 .get(&TypeId::of::<T>())
831 .and_then(|value| value.downcast_ref::<T>())
832 }
833
834 pub fn require_resource<T>(&self) -> Result<&T, ContextError>
835 where
836 T: Send + Sync + 'static,
837 {
838 self.get_resource::<T>()
839 .ok_or(ContextError::MissingTypedResource(
840 std::any::type_name::<T>(),
841 ))
842 }
843
844 pub fn insert_named_resource<T>(&mut self, name: impl Into<String>, resource: T)
845 where
846 T: Send + Sync + 'static,
847 {
848 self.named_resources.insert(name.into(), Box::new(resource));
849 }
850
851 pub fn get_named_resource<T>(&self, name: &str) -> Option<&T>
852 where
853 T: Send + Sync + 'static,
854 {
855 self.named_resources
856 .get(name)
857 .and_then(|value| value.downcast_ref::<T>())
858 }
859
860 pub fn require_named_resource<T>(&self, name: &str) -> Result<&T, ContextError>
861 where
862 T: Send + Sync + 'static,
863 {
864 self.get_named_resource::<T>(name)
865 .ok_or_else(|| ContextError::MissingResource(name.to_owned()))
866 }
867
868 pub fn put_local(&mut self, key: impl Into<String>, value: impl Into<Value>) {
869 self.locals.insert(key.into(), value.into());
870 }
871
872 pub fn local(&self, key: &str) -> Option<&Value> {
873 self.locals.get(key)
874 }
875
876 pub fn remove_local(&mut self, key: &str) -> Option<Value> {
877 self.locals.remove(key)
878 }
879
880 pub fn has_entity_data_service(&self, entity: &str) -> bool {
881 let in_registry = self
882 .entity_registry
883 .as_ref()
884 .map(|registry| registry.contains(entity))
885 .unwrap_or(false);
886 in_registry || self.entity(entity).is_some()
887 }
888
889 pub fn entity_data_service_behavior(
890 &self,
891 entity: &str,
892 ) -> Option<std::sync::Arc<dyn EntityDataServiceBehavior>> {
893 self.entity_data_service_behavior_registry
894 .as_ref()
895 .and_then(|registry| registry.behavior(entity))
896 }
897
898 pub fn has_checker(&self, entity: &str) -> bool {
899 self.checker_registry
900 .as_ref()
901 .and_then(|registry| registry.checker(entity))
902 .is_some()
903 }
904
905 pub fn check_and_fix_record(
906 &self,
907 entity: &str,
908 record: &mut Record,
909 ) -> Result<(), RuntimeError> {
910 self.check_and_fix_record_at(entity, record, &ObjectLocation::root())
911 }
912
913 pub fn check_and_fix_record_at(
914 &self,
915 entity: &str,
916 record: &mut Record,
917 location: &ObjectLocation,
918 ) -> Result<(), RuntimeError> {
919 let status = CheckObjectStatus::from_record(record);
920 let checker = self
921 .checker_registry
922 .as_ref()
923 .and_then(|registry| registry.checker(entity));
924 let mut results = CheckResults::new();
925 if let Some(checker) = checker {
926 checker.check_and_fix(self, record, location, &mut results);
927 }
928
929 if let Some(descriptor) = self
934 .metadata
935 .as_ref()
936 .and_then(|metadata| metadata.entity(entity))
937 {
938 for property in descriptor
939 .properties
940 .iter()
941 .filter(|property| !property.nullable)
942 {
943 let missing = !record.contains_key(&property.name);
944 let null = matches!(record.get(&property.name), Some(Value::Null));
945 let property_location = location.clone().member(&property.name);
946 let already_reported = results.iter().any(|result| {
947 result.rule == crate::CheckRule::Required
948 && result.location == property_location
949 });
950 if ((status.is_create() && missing) || null) && !already_reported {
951 results.push(CheckResult::required(property_location));
952 }
953 }
954 }
955 if results.is_empty() {
956 return Ok(());
957 }
958 self.translate_check_results(&mut results);
959 Err(RuntimeError::Check(results))
960 }
961
962 pub fn translate_check_results(&self, results: &mut CheckResults) {
963 for result in results {
964 result.message = Some(translate_check_result(self.language, result));
965 }
966 }
967
968 pub fn send_event(&self, event: RawAuditEvent) -> Result<(), RuntimeError> {
969 if let Some(sink) = self.event_sink.as_ref() {
970 sink.on_event(self, &event)?;
971 }
972 if let Some(sink) = self.custom_event_sink.as_ref() {
973 let (mask_fields, max_len) = self
974 .metadata
975 .as_ref()
976 .and_then(|metadata| metadata.entity(&event.entity))
977 .map(|desc| (desc.audit_mask_fields.clone(), desc.audit_value_max_len))
978 .unwrap_or_else(|| (vec![], None));
979
980 let safe_event = event.build_safe_event(&mask_fields, max_len);
981 sink.on_safe_event(self, &safe_event)?;
982 }
983
984 crate::log_formatter::LogManager::write_audit_log(&event);
985
986 Ok(())
987 }
988
989 pub(crate) async fn commit_changes_internal<E>(&self) -> Result<(), DataServiceError<E::Error>>
990 where
991 E: teaql_data_service::MutationExecutor + Send + Sync + 'static,
992 {
993 let executor = self.require_resource::<E>().map_err(|err| {
994 DataServiceError::Runtime(RuntimeError::Graph(format!(
995 "cannot commit changes without executor: {err}"
996 )))
997 })?;
998 let change_set = self.entity_root.current_change_set();
999
1000 for (key, changes) in change_set.changes() {
1001 if changes.is_empty() {
1002 continue;
1003 }
1004 let _entity = self
1005 .require_entity(&key.entity)
1006 .map_err(DataServiceError::Runtime)?;
1007 let mut command = UpdateCommand::new(&key.entity, key.id.clone());
1008 for (field, value) in changes {
1009 command = command.value(field.clone(), value.clone());
1010 }
1011 let request = teaql_data_service::MutationRequest::Update(command);
1012 executor
1013 .mutate(request)
1014 .await
1015 .map_err(DataServiceError::Executor)?;
1016 }
1017
1018 self.entity_root.clear_current_change_set();
1019 Ok(())
1020 }
1021
1022 pub async fn get_in_store(&self, key: &str) -> Option<Value> {
1023 let store = self.get_resource::<Box<dyn DataStore>>()?;
1024 store.get(key).await
1025 }
1026
1027 pub async fn put_in_store(
1028 &self,
1029 key: &str,
1030 value: impl Into<Value>,
1031 timeout_seconds: Option<u64>,
1032 ) {
1033 if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1034 store.put(key, value.into(), timeout_seconds).await;
1035 }
1036 }
1037
1038 pub async fn clear_in_store(&self, key: &str) {
1039 if let Some(store) = self.get_resource::<Box<dyn DataStore>>() {
1040 store.remove(key).await;
1041 }
1042 }
1043}
1044
1045fn extract_id_from_sql(sql: &str) -> Option<String> {
1046 let sql_lower = sql.to_lowercase();
1047 let where_idx = sql_lower.find("where")?;
1048 let where_clause = &sql_lower[where_idx + 5..];
1049
1050 let bytes = where_clause.as_bytes();
1051 let mut i = 0;
1052 while i < bytes.len() {
1053 if i + 1 < bytes.len() && &bytes[i..i + 2] == b"id" {
1054 let prev_ok = i == 0 || {
1056 let prev_char = bytes[i - 1] as char;
1057 !prev_char.is_ascii_alphanumeric() && prev_char != '_' && prev_char != '.'
1058 };
1059 let next_ok = i + 2 == bytes.len() || {
1061 let next_char = bytes[i + 2] as char;
1062 !next_char.is_ascii_alphanumeric() && next_char != '_'
1063 };
1064
1065 if prev_ok && next_ok {
1066 let mut j = i + 2;
1069 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1070 j += 1;
1071 }
1072 if j < bytes.len() && bytes[j] == b'=' {
1073 j += 1;
1074 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
1075 j += 1;
1076 }
1077 let mut val_str = String::new();
1079 if j < bytes.len() && bytes[j] == b'\'' {
1080 j += 1; while j < bytes.len() && bytes[j] != b'\'' {
1082 val_str.push(bytes[j] as char);
1083 j += 1;
1084 }
1085 return Some(val_str);
1086 }
1087 while j < bytes.len() {
1089 let c = bytes[j] as char;
1090 if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
1091 break;
1092 }
1093 val_str.push(c);
1094 j += 1;
1095 }
1096 if !val_str.is_empty() {
1097 return Some(val_str);
1098 }
1099 }
1100 }
1101 }
1102 i += 1;
1103 }
1104 None
1105}
1106
1107fn sql_result_summary(
1108 operation: SqlLogOperation,
1109 result_count: Option<usize>,
1110 result_type: Option<&str>,
1111 affected_rows: Option<u64>,
1112 debug_sql: &str,
1113) -> String {
1114 match operation {
1115 SqlLogOperation::Select => {
1116 let count = result_count.unwrap_or(0);
1117 match count {
1118 0 => "MISS".to_owned(),
1119 1 => match result_type {
1120 Some(result_type) => extract_id_from_sql(debug_sql)
1121 .map(|id| format!("{result_type}({id})"))
1122 .unwrap_or_else(|| result_type.to_owned()),
1123 None => "row".to_owned(),
1124 },
1125 _ => match result_type {
1126 Some(result_type) => format!("{count}*{result_type}"),
1127 None => format!("{count}*rows"),
1128 },
1129 }
1130 }
1131 _ => {
1132 let affected = affected_rows.unwrap_or(0);
1133 format!("{affected} UPDATED")
1134 }
1135 }
1136}
1137
1138fn pretty_sql(sql: &str) -> String {
1139 let mut pretty = sql.to_owned();
1140 for keyword in [
1141 " FROM ",
1142 " WHERE ",
1143 " GROUP BY ",
1144 " HAVING ",
1145 " ORDER BY ",
1146 " LIMIT ",
1147 " OFFSET ",
1148 " RETURNING ",
1149 ] {
1150 pretty = pretty.replace(keyword, &format!("\n{}", keyword.trim_start()));
1151 }
1152 pretty.replace(" AND ", "\n AND ")
1153}