Skip to main content

teaql_runtime/
context.rs

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            // Keep sql_log_entries backwards-compatible for now if needed,
415            // wait, we modified SqlLogEntry. We can just push it directly since we removed comment.
416            // Wait, we need to push a cloned SqlLogEntry since it doesn't have comment.
417            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, // Approximate
441                    teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
442                    teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
443                },
444                sql: String::new(), // Not available in metadata
445                params: Vec::new(), // Not available in metadata
446                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, // Not directly available
453                affected_rows: metadata.affected_rows,
454                result_summary: String::new(), // We can synthesize this if needed, or leave it empty/basic
455            };
456
457            // synthesize a summary for the log
458            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            // Check boundary before
706            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            // Check boundary after
713            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                // Found the standalone "id" word!
722                // Now look for "=" after it
723                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                      // Now extract the value
733                      let mut val_str = String::new();
734                      if j < bytes.len() && bytes[j] == b'\'' {
735                          j += 1; // consume single quote
736                          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