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    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            // Keep sql_log_entries backwards-compatible for now if needed,
430            // wait, we modified SqlLogEntry. We can just push it directly since we removed comment.
431            // Wait, we need to push a cloned SqlLogEntry since it doesn't have comment.
432            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, // Approximate
456                    teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
457                    teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
458                },
459                sql: String::new(), // Not available in metadata
460                params: Vec::new(), // Not available in metadata
461                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, // Not directly available
468                affected_rows: metadata.affected_rows,
469                result_summary: String::new(), // We can synthesize this if needed, or leave it empty/basic
470            };
471
472            // synthesize a summary for the log
473            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            // Check boundary before
721            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            // Check boundary after
728            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                // Found the standalone "id" word!
737                // Now look for "=" after it
738                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                      // Now extract the value
748                      let mut val_str = String::new();
749                      if j < bytes.len() && bytes[j] == b'\'' {
750                          j += 1; // consume single quote
751                          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