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