1use std::any::{Any, TypeId};
2use std::collections::{BTreeMap, HashMap};
3use std::future::Future;
4
5use std::pin::Pin;
6use std::sync::Mutex;
7use std::time::{Duration, SystemTime};
8
9use teaql_core::{EntityDescriptor, Record, UpdateCommand, Value};
10use teaql_sql::{CompiledQuery, DatabaseKind};
11
12use crate::{
13 CheckResults, CheckerRegistry, ContextError, EntityEvent, EntityEventSink, GraphNode,
14 InternalIdGenerator, Language, MetadataStore, ObjectLocation, RepositoryBehavior,
15 RepositoryBehaviorRegistry, RepositoryRegistry, RequestPolicy, RuntimeError,
16 local_id_generator, translate_check_result,
17};
18use crate::{EntityRoot, RepositoryError};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum SqlLogOperation {
22 Select,
23 Insert,
24 Update,
25 Delete,
26 Recover,
27}
28
29impl SqlLogOperation {
30 pub fn is_select(self) -> bool {
31 matches!(self, Self::Select)
32 }
33
34 pub fn is_mutation(self) -> bool {
35 !self.is_select()
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub struct SqlLogOptions {
41 pub select: bool,
42 pub mutation: bool,
43}
44
45impl SqlLogOptions {
46 pub fn disabled() -> Self {
47 Self {
48 select: false,
49 mutation: false,
50 }
51 }
52
53 pub fn select_only() -> Self {
54 Self {
55 select: true,
56 mutation: false,
57 }
58 }
59
60 pub fn mutation_only() -> Self {
61 Self {
62 select: false,
63 mutation: true,
64 }
65 }
66
67 pub fn all() -> Self {
68 Self {
69 select: true,
70 mutation: true,
71 }
72 }
73
74 pub fn enabled_for(self, operation: SqlLogOperation) -> bool {
75 if operation.is_select() {
76 self.select
77 } else {
78 self.mutation
79 }
80 }
81}
82
83#[derive(Debug, Clone, PartialEq)]
84pub struct SqlLogEntry {
85 pub operation: SqlLogOperation,
86 pub sql: String,
87 pub params: Vec<Value>,
88 pub debug_sql: String,
89 pub pretty_sql: String,
90 pub started_at: SystemTime,
91 pub ended_at: SystemTime,
92 pub elapsed: Duration,
93 pub result_count: Option<usize>,
94 pub result_type: Option<String>,
95 pub affected_rows: Option<u64>,
96 pub result_summary: String,
97}
98
99#[derive(Debug, Clone, PartialEq)]
100pub struct UnifiedLogEntry {
101 pub timestamp: SystemTime,
102 pub user_identifier: Option<String>,
103 pub trace_chain: Vec<teaql_core::TraceNode>,
104 pub payload: LogPayload,
105}
106
107#[derive(Debug, Clone, PartialEq)]
108pub enum LogPayload {
109 Sql(SqlLogEntry),
110 Info(InfoLogEntry),
111}
112
113#[derive(Debug, Clone, PartialEq)]
114pub struct InfoLogEntry {
115 pub message: String,
116}
117
118#[derive(Clone, Default)]
119pub struct UnifiedLogBuffer {
120 pub entries: std::sync::Arc<Mutex<Vec<UnifiedLogEntry>>>,
121}
122
123pub trait SchemaProvider: Send + Sync {
124 fn ensure_schema<'a>(
125 &'a self,
126 ctx: &'a UserContext,
127 ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>>;
128}
129
130pub struct UserContext {
131 pub(crate) metadata: Option<Box<dyn MetadataStore>>,
132 pub(crate) repository_registry: Option<Box<dyn RepositoryRegistry>>,
133 pub(crate) repository_behavior_registry: Option<Box<dyn RepositoryBehaviorRegistry>>,
134 pub(crate) request_policy: Option<Box<dyn RequestPolicy>>,
135 pub(crate) checker_registry: Option<Box<dyn CheckerRegistry>>,
136 pub(crate) event_sink: Option<Box<dyn EntityEventSink>>,
137 pub(crate) internal_id_generator: Option<Box<dyn InternalIdGenerator>>,
138 schema_provider: Option<Box<dyn SchemaProvider>>,
139 language: Language,
140 typed_resources: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
141 named_resources: BTreeMap<String, Box<dyn Any + Send + Sync>>,
142 locals: BTreeMap<String, Value>,
143 pub(crate) initial_graphs: Vec<GraphNode>,
144 entity_root: EntityRoot,
145 sql_log_options: SqlLogOptions,
146 sql_log_entries: Mutex<Vec<SqlLogEntry>>,
147 user_identifier: Option<String>,
148 timezone: Option<String>,
149}
150
151impl Default for UserContext {
152 fn default() -> Self {
153 let pid = std::process::id();
154 let thread_id_str = format!("{:?}", std::thread::current().id());
155 let numeric_thread_id = thread_id_str
156 .strip_prefix("ThreadId(")
157 .and_then(|s| s.strip_suffix(")"))
158 .unwrap_or(&thread_id_str);
159 let os_user = std::env::var("USER")
160 .or_else(|_| std::env::var("USERNAME"))
161 .unwrap_or_else(|_| "main".to_owned());
162 let user_id = format!("{os_user}@pid-{pid}.tid-{numeric_thread_id}");
163 Self {
164 metadata: None,
165 repository_registry: None,
166 repository_behavior_registry: None,
167 request_policy: None,
168 checker_registry: None,
169 event_sink: None,
170 internal_id_generator: None,
171 schema_provider: None,
172 language: Language::default(),
173 typed_resources: HashMap::new(),
174 named_resources: BTreeMap::new(),
175 locals: BTreeMap::new(),
176 initial_graphs: Vec::new(),
177 entity_root: EntityRoot::default(),
178 sql_log_options: SqlLogOptions::all(),
179 sql_log_entries: Mutex::new(Vec::new()),
180 user_identifier: Some(user_id),
181 timezone: Some("UTC".to_owned()),
182 }
183 }
184}
185
186impl UserContext {
187 pub fn new() -> Self {
188 Self::default()
189 }
190
191 pub fn user_identifier(&self) -> Option<&str> {
192 self.user_identifier.as_deref()
193 }
194
195 pub fn set_user_identifier(&mut self, user_identifier: impl Into<String>) {
196 self.user_identifier = Some(user_identifier.into());
197 }
198
199 pub fn with_user_identifier(mut self, user_identifier: impl Into<String>) -> Self {
200 self.user_identifier = Some(user_identifier.into());
201 self
202 }
203
204 pub fn set_user_identifier_option(&mut self, user_identifier: Option<String>) {
205 self.user_identifier = user_identifier;
206 }
207
208 pub fn with_user_identifier_option(mut self, user_identifier: Option<String>) -> Self {
209 self.user_identifier = user_identifier;
210 self
211 }
212
213 pub fn timezone(&self) -> Option<&str> {
214 self.timezone.as_deref()
215 }
216
217 pub fn set_timezone(&mut self, timezone: impl Into<String>) {
218 self.timezone = Some(timezone.into());
219 }
220
221 pub fn with_timezone(mut self, timezone: impl Into<String>) -> Self {
222 self.timezone = Some(timezone.into());
223 self
224 }
225
226 pub fn with_module(mut self, module: crate::RuntimeModule) -> Self {
227 module.apply_to(&mut self);
228 self
229 }
230
231 pub fn entity_root(&self) -> EntityRoot {
232 self.entity_root.clone()
233 }
234
235 pub fn initial_graphs(&self) -> &[GraphNode] {
236 &self.initial_graphs
237 }
238
239 pub fn set_initial_graphs(&mut self, graphs: Vec<GraphNode>) {
240 self.initial_graphs = graphs;
241 }
242
243 pub fn with_metadata(mut self, metadata: impl MetadataStore + 'static) -> Self {
244 self.metadata = Some(Box::new(metadata));
245 self
246 }
247
248 pub fn set_metadata(&mut self, metadata: impl MetadataStore + 'static) {
249 self.metadata = Some(Box::new(metadata));
250 }
251
252 pub fn with_repository_registry(mut self, registry: impl RepositoryRegistry + 'static) -> Self {
253 self.repository_registry = Some(Box::new(registry));
254 self
255 }
256
257 pub fn set_repository_registry(&mut self, registry: impl RepositoryRegistry + 'static) {
258 self.repository_registry = Some(Box::new(registry));
259 }
260
261 pub fn with_repository_behavior_registry(
262 mut self,
263 registry: impl RepositoryBehaviorRegistry + 'static,
264 ) -> Self {
265 self.repository_behavior_registry = Some(Box::new(registry));
266 self
267 }
268
269 pub fn set_repository_behavior_registry(
270 &mut self,
271 registry: impl RepositoryBehaviorRegistry + 'static,
272 ) {
273 self.repository_behavior_registry = Some(Box::new(registry));
274 }
275
276 pub fn with_request_policy(mut self, policy: impl RequestPolicy + 'static) -> Self {
277 self.request_policy = Some(Box::new(policy));
278 self
279 }
280
281 pub fn set_request_policy(&mut self, policy: impl RequestPolicy + 'static) {
282 self.request_policy = Some(Box::new(policy));
283 }
284
285 pub fn clear_request_policy(&mut self) {
286 self.request_policy = None;
287 }
288
289 pub fn with_checker_registry(mut self, registry: impl CheckerRegistry + 'static) -> Self {
290 self.checker_registry = Some(Box::new(registry));
291 self
292 }
293
294 pub fn set_checker_registry(&mut self, registry: impl CheckerRegistry + 'static) {
295 self.checker_registry = Some(Box::new(registry));
296 }
297
298 pub fn with_event_sink(mut self, sink: impl EntityEventSink + 'static) -> Self {
299 self.event_sink = Some(Box::new(sink));
300 self
301 }
302
303 pub fn set_event_sink(&mut self, sink: impl EntityEventSink + 'static) {
304 self.event_sink = Some(Box::new(sink));
305 }
306
307 pub fn with_internal_id_generator(
308 mut self,
309 generator: impl InternalIdGenerator + 'static,
310 ) -> Self {
311 self.internal_id_generator = Some(Box::new(generator));
312 self
313 }
314
315 pub fn set_internal_id_generator(&mut self, generator: impl InternalIdGenerator + 'static) {
316 self.internal_id_generator = Some(Box::new(generator));
317 }
318
319 pub fn with_schema_provider(mut self, provider: impl SchemaProvider + 'static) -> Self {
320 self.schema_provider = Some(Box::new(provider));
321 self
322 }
323
324 pub fn set_schema_provider(&mut self, provider: impl SchemaProvider + 'static) {
325 self.schema_provider = Some(Box::new(provider));
326 }
327
328 pub async fn ensure_schema(&self) -> Result<(), RuntimeError> {
329 let provider = self
330 .schema_provider
331 .as_ref()
332 .ok_or_else(|| RuntimeError::Schema("missing schema provider".to_owned()))?;
333 provider.ensure_schema(self).await
334 }
335
336 pub fn with_language(mut self, language: Language) -> Self {
337 self.language = language;
338 self
339 }
340
341 pub fn set_language(&mut self, language: Language) {
342 self.language = language;
343 }
344
345 pub fn with_sql_log_options(mut self, options: SqlLogOptions) -> Self {
346 self.sql_log_options = options;
347 self
348 }
349
350 pub fn set_sql_log_options(&mut self, options: SqlLogOptions) {
351 self.sql_log_options = options;
352 }
353
354 pub fn enable_select_sql_log(&mut self) {
355 self.sql_log_options.select = true;
356 }
357
358 pub fn enable_mutation_sql_log(&mut self) {
359 self.sql_log_options.mutation = true;
360 }
361
362 pub fn enable_all_sql_log(&mut self) {
363 self.sql_log_options = SqlLogOptions::all();
364 }
365
366 pub fn disable_sql_log(&mut self) {
367 self.sql_log_options = SqlLogOptions::disabled();
368 self.clear_sql_logs();
369 }
370
371 pub fn sql_log_options(&self) -> SqlLogOptions {
372 self.sql_log_options
373 }
374
375 pub fn sql_logs(&self) -> Vec<SqlLogEntry> {
376 self.sql_log_entries
377 .lock()
378 .map(|entries| entries.clone())
379 .unwrap_or_default()
380 }
381
382 pub fn clear_sql_logs(&self) {
383 if let Ok(mut entries) = self.sql_log_entries.lock() {
384 entries.clear();
385 }
386 }
387
388 pub(crate) fn record_sql_log(
389 &self,
390 operation: SqlLogOperation,
391 query: &CompiledQuery,
392 database_kind: DatabaseKind,
393 started_at: SystemTime,
394 ended_at: SystemTime,
395 elapsed: Duration,
396 result_count: Option<usize>,
397 result_type: Option<String>,
398 affected_rows: Option<u64>,
399 trace_chain: Vec<teaql_core::TraceNode>,
400 ) {
401 if !self.sql_log_options.enabled_for(operation) {
402 return;
403 }
404 let debug_sql = query.debug_sql(database_kind);
405 let result_summary = sql_result_summary(
406 operation,
407 result_count,
408 result_type.as_deref(),
409 affected_rows,
410 &debug_sql,
411 );
412
413 let sql_log_entry = SqlLogEntry {
414 operation,
415 sql: query.sql.clone(),
416 params: query.params.clone(),
417 pretty_sql: pretty_sql(&debug_sql),
418 debug_sql: debug_sql.clone(),
419 started_at,
420 ended_at,
421 elapsed,
422 result_summary: result_summary.clone(),
423 result_count,
424 result_type,
425 affected_rows,
426 };
427
428 if let Ok(mut entries) = self.sql_log_entries.lock() {
429 entries.push(sql_log_entry.clone());
433 }
434
435 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
436 if let Ok(mut entries) = buf.entries.lock() {
437 entries.push(UnifiedLogEntry {
438 timestamp: started_at,
439 user_identifier: self.user_identifier.clone(),
440 trace_chain,
441 payload: LogPayload::Sql(sql_log_entry),
442 });
443 }
444 }
445 }
446
447 pub(crate) fn record_metadata_log(&self, metadata: &teaql_data_service::ExecutionMetadata) {
448 if let Some(debug_sql) = &metadata.debug_query {
449 let sql_log_entry = SqlLogEntry {
450 operation: match metadata.operation {
451 teaql_data_service::DataServiceOperation::Query => SqlLogOperation::Select,
452 teaql_data_service::DataServiceOperation::Insert => SqlLogOperation::Insert,
453 teaql_data_service::DataServiceOperation::Update => SqlLogOperation::Update,
454 teaql_data_service::DataServiceOperation::Delete => SqlLogOperation::Delete,
455 teaql_data_service::DataServiceOperation::Recover => SqlLogOperation::Update, teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
457 teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
458 },
459 sql: String::new(), params: Vec::new(), pretty_sql: pretty_sql(debug_sql),
462 debug_sql: debug_sql.clone(),
463 started_at: metadata.started_at,
464 ended_at: metadata.ended_at,
465 elapsed: metadata.ended_at.duration_since(metadata.started_at).unwrap_or_default(),
466 result_count: metadata.result_count,
467 result_type: None, affected_rows: metadata.affected_rows,
469 result_summary: String::new(), };
471
472 let mut summary = String::new();
474 if let Some(c) = metadata.result_count {
475 summary = format!("{} rows returned", c);
476 } else if let Some(a) = metadata.affected_rows {
477 summary = format!("{} rows affected", a);
478 }
479
480 let mut final_entry = sql_log_entry;
481 final_entry.result_summary = summary;
482
483 if let Ok(mut entries) = self.sql_log_entries.lock() {
484 entries.push(final_entry.clone());
485 }
486
487 if let Some(buf) = self.get_resource::<UnifiedLogBuffer>() {
488 if let Ok(mut entries) = buf.entries.lock() {
489 entries.push(UnifiedLogEntry {
490 timestamp: metadata.started_at,
491 user_identifier: self.user_identifier.clone(),
492 trace_chain: metadata.trace_chain.clone(),
493 payload: LogPayload::Sql(final_entry),
494 });
495 }
496 }
497 }
498 }
499
500 pub fn language(&self) -> Language {
501 self.language
502 }
503
504 pub fn set_language_code(&mut self, code: &str) -> Result<(), RuntimeError> {
505 let Some(language) = Language::from_code(code) else {
506 return Err(RuntimeError::Language(format!(
507 "unsupported language code: {code}"
508 )));
509 };
510 self.language = language;
511 Ok(())
512 }
513
514 pub fn generate_id(&self, entity: &str) -> Result<Option<u64>, RuntimeError> {
515 self.internal_id_generator
516 .as_ref()
517 .map(|generator| generator.generate_id(entity))
518 .transpose()
519 }
520
521 pub fn next_id(&self, entity: &str) -> Result<u64, RuntimeError> {
522 match self.generate_id(entity)? {
523 Some(id) => Ok(id),
524 None => local_id_generator().generate_id(entity),
525 }
526 }
527
528 pub fn entity(&self, name: &str) -> Option<&EntityDescriptor> {
529 self.metadata
530 .as_ref()
531 .and_then(|metadata| metadata.entity(name))
532 }
533
534 pub fn all_entities(&self) -> Vec<&EntityDescriptor> {
535 self.metadata
536 .as_ref()
537 .map(|metadata| metadata.all_entities())
538 .unwrap_or_default()
539 }
540
541 pub fn require_entity(&self, name: &str) -> Result<&EntityDescriptor, RuntimeError> {
542 self.entity(name)
543 .ok_or_else(|| RuntimeError::MissingEntity(name.to_owned()))
544 }
545
546 pub fn insert_resource<T>(&mut self, resource: T)
547 where
548 T: Send + Sync + 'static,
549 {
550 self.typed_resources
551 .insert(TypeId::of::<T>(), Box::new(resource));
552 }
553
554 pub fn get_resource<T>(&self) -> Option<&T>
555 where
556 T: Send + Sync + 'static,
557 {
558 self.typed_resources
559 .get(&TypeId::of::<T>())
560 .and_then(|value| value.downcast_ref::<T>())
561 }
562
563 pub fn require_resource<T>(&self) -> Result<&T, ContextError>
564 where
565 T: Send + Sync + 'static,
566 {
567 self.get_resource::<T>()
568 .ok_or(ContextError::MissingTypedResource(
569 std::any::type_name::<T>(),
570 ))
571 }
572
573 pub fn insert_named_resource<T>(&mut self, name: impl Into<String>, resource: T)
574 where
575 T: Send + Sync + 'static,
576 {
577 self.named_resources.insert(name.into(), Box::new(resource));
578 }
579
580 pub fn get_named_resource<T>(&self, name: &str) -> Option<&T>
581 where
582 T: Send + Sync + 'static,
583 {
584 self.named_resources
585 .get(name)
586 .and_then(|value| value.downcast_ref::<T>())
587 }
588
589 pub fn require_named_resource<T>(&self, name: &str) -> Result<&T, ContextError>
590 where
591 T: Send + Sync + 'static,
592 {
593 self.get_named_resource::<T>(name)
594 .ok_or_else(|| ContextError::MissingResource(name.to_owned()))
595 }
596
597 pub fn put_local(&mut self, key: impl Into<String>, value: impl Into<Value>) {
598 self.locals.insert(key.into(), value.into());
599 }
600
601 pub fn local(&self, key: &str) -> Option<&Value> {
602 self.locals.get(key)
603 }
604
605 pub fn remove_local(&mut self, key: &str) -> Option<Value> {
606 self.locals.remove(key)
607 }
608
609 pub fn has_repository(&self, entity: &str) -> bool {
610 let in_registry = self
611 .repository_registry
612 .as_ref()
613 .map(|registry| registry.contains(entity))
614 .unwrap_or(false);
615 in_registry || self.entity(entity).is_some()
616 }
617
618 pub fn repository_behavior(
619 &self,
620 entity: &str,
621 ) -> Option<std::sync::Arc<dyn RepositoryBehavior>> {
622 self.repository_behavior_registry
623 .as_ref()
624 .and_then(|registry| registry.behavior(entity))
625 }
626
627 pub fn has_checker(&self, entity: &str) -> bool {
628 self.checker_registry
629 .as_ref()
630 .and_then(|registry| registry.checker(entity))
631 .is_some()
632 }
633
634 pub fn check_and_fix_record(
635 &self,
636 entity: &str,
637 record: &mut Record,
638 ) -> Result<(), RuntimeError> {
639 self.check_and_fix_record_at(entity, record, &ObjectLocation::root())
640 }
641
642 pub fn check_and_fix_record_at(
643 &self,
644 entity: &str,
645 record: &mut Record,
646 location: &ObjectLocation,
647 ) -> Result<(), RuntimeError> {
648 let Some(checker) = self
649 .checker_registry
650 .as_ref()
651 .and_then(|registry| registry.checker(entity))
652 else {
653 return Ok(());
654 };
655 let mut results = CheckResults::new();
656 checker.check_and_fix(self, record, location, &mut results);
657 if results.is_empty() {
658 Ok(())
659 } else {
660 self.translate_check_results(&mut results);
661 Err(RuntimeError::Check(results))
662 }
663 }
664
665 pub fn translate_check_results(&self, results: &mut CheckResults) {
666 for result in results {
667 result.message = Some(translate_check_result(self.language, result));
668 }
669 }
670
671 pub fn send_event(&self, event: EntityEvent) -> Result<(), RuntimeError> {
672 let Some(sink) = self.event_sink.as_ref() else {
673 return Ok(());
674 };
675 sink.on_event(self, &event)
676 }
677
678 pub async fn commit_changes<E>(&self) -> Result<(), RepositoryError<E::Error>>
679 where
680 E: teaql_data_service::MutationExecutor + Send + Sync + 'static,
681 {
682 let executor = self.require_resource::<E>().map_err(|err| {
683 RepositoryError::Runtime(RuntimeError::Graph(format!(
684 "cannot commit changes without executor: {err}"
685 )))
686 })?;
687 let change_set = self.entity_root.current_change_set();
688
689 for (key, changes) in change_set.changes() {
690 if changes.is_empty() {
691 continue;
692 }
693 let _entity = self
694 .require_entity(&key.entity)
695 .map_err(RepositoryError::Runtime)?;
696 let mut command = UpdateCommand::new(&key.entity, key.id.clone());
697 for (field, value) in changes {
698 command = command.value(field.clone(), value.clone());
699 }
700 let request = teaql_data_service::MutationRequest::Update(command);
701 executor
702 .mutate(request).await
703 .map_err(RepositoryError::Executor)?;
704 }
705
706 self.entity_root.clear_current_change_set();
707 Ok(())
708 }
709}
710
711fn extract_id_from_sql(sql: &str) -> Option<String> {
712 let sql_lower = sql.to_lowercase();
713 let where_idx = sql_lower.find("where")?;
714 let where_clause = &sql_lower[where_idx + 5..];
715
716 let bytes = where_clause.as_bytes();
717 let mut i = 0;
718 while i < bytes.len() {
719 if i + 1 < bytes.len() && &bytes[i..i+2] == b"id" {
720 let prev_ok = if i == 0 {
722 true
723 } else {
724 let prev_char = bytes[i - 1] as char;
725 !prev_char.is_ascii_alphanumeric() && prev_char != '_' && prev_char != '.'
726 };
727 let next_ok = if i + 2 == bytes.len() {
729 true
730 } else {
731 let next_char = bytes[i + 2] as char;
732 !next_char.is_ascii_alphanumeric() && next_char != '_'
733 };
734
735 if prev_ok && next_ok {
736 let mut j = i + 2;
739 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
740 j += 1;
741 }
742 if j < bytes.len() && bytes[j] == b'=' {
743 j += 1;
744 while j < bytes.len() && (bytes[j] as char).is_whitespace() {
745 j += 1;
746 }
747 let mut val_str = String::new();
749 if j < bytes.len() && bytes[j] == b'\'' {
750 j += 1; while j < bytes.len() && bytes[j] != b'\'' {
752 val_str.push(bytes[j] as char);
753 j += 1;
754 }
755 return Some(val_str);
756 } else {
757 while j < bytes.len() {
758 let c = bytes[j] as char;
759 if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
760 val_str.push(c);
761 j += 1;
762 } else {
763 break;
764 }
765 }
766 if !val_str.is_empty() {
767 return Some(val_str);
768 }
769 }
770 }
771 }
772 }
773 i += 1;
774 }
775 None
776}
777
778fn sql_result_summary(
779 operation: SqlLogOperation,
780 result_count: Option<usize>,
781 result_type: Option<&str>,
782 affected_rows: Option<u64>,
783 debug_sql: &str,
784) -> String {
785 match operation {
786 SqlLogOperation::Select => {
787 let count = result_count.unwrap_or(0);
788 if count == 0 {
789 "MISS".to_owned()
790 } else if count > 1 {
791 match result_type {
792 Some(result_type) => format!("{count}*{result_type}"),
793 None => format!("{count}*rows"),
794 }
795 } else {
796 match result_type {
797 Some(result_type) => {
798 if let Some(id) = extract_id_from_sql(debug_sql) {
799 format!("{result_type}({id})")
800 } else {
801 result_type.to_owned()
802 }
803 }
804 None => "row".to_owned(),
805 }
806 }
807 }
808 _ => {
809 let affected = affected_rows.unwrap_or(0);
810 format!("{affected} UPDATED")
811 }
812 }
813}
814
815fn pretty_sql(sql: &str) -> String {
816 let mut pretty = sql.to_owned();
817 for keyword in [
818 " FROM ",
819 " WHERE ",
820 " GROUP BY ",
821 " HAVING ",
822 " ORDER BY ",
823 " LIMIT ",
824 " OFFSET ",
825 " RETURNING ",
826 ] {
827 pretty = pretty.replace(keyword, &format!("\n{}", keyword.trim_start()));
828 }
829 pretty
830 .replace(" AND ", "\n AND ")
831 .replace(" OR ", "\n OR ")
832}
833
834