Skip to main content

scouter_sql/sql/
utils.rs

1use crate::sql::error::SqlError;
2use chrono::{DateTime, Utc};
3use scouter_types::{
4    CustomMetricRecord, GenAIEvalRecord, GenAIEvalTaskResult, GenAIEvalWorkflowResult,
5    IntoServerRecord, PsiRecord, RecordType, ServerRecords, SpcRecord,
6};
7use sqlx::postgres::PgRow;
8use std::str::FromStr;
9use uuid::Uuid;
10/// Generic function to deserialize PgRows into ServerRecords
11pub fn pg_rows_to_server_records<T>(
12    rows: &[PgRow],
13    _record_type: &RecordType,
14) -> Result<ServerRecords, SqlError>
15where
16    T: for<'r> sqlx::FromRow<'r, PgRow> + IntoServerRecord + Send + Unpin,
17{
18    let mut records = Vec::with_capacity(rows.len());
19
20    for row in rows {
21        let record: T = sqlx::FromRow::from_row(row)?;
22        records.push(record.into_server_record());
23    }
24
25    Ok(ServerRecords::new(records))
26}
27
28/// Parses Postgres rows into ServerRecords based on RecordType
29pub fn parse_pg_rows(
30    rows: &[sqlx::postgres::PgRow],
31    record_type: &RecordType,
32) -> Result<ServerRecords, SqlError> {
33    match record_type {
34        RecordType::Spc => pg_rows_to_server_records::<SpcRecord>(rows, record_type),
35        RecordType::Psi => {
36            crate::sql::utils::pg_rows_to_server_records::<PsiRecord>(rows, record_type)
37        }
38        RecordType::Custom => {
39            crate::sql::utils::pg_rows_to_server_records::<CustomMetricRecord>(rows, record_type)
40        }
41        RecordType::GenAIEval => {
42            crate::sql::utils::pg_rows_to_server_records::<GenAIEvalRecord>(rows, record_type)
43        }
44        RecordType::GenAITask => {
45            crate::sql::utils::pg_rows_to_server_records::<GenAIEvalTaskResult>(rows, record_type)
46        }
47        RecordType::GenAIWorkflow => crate::sql::utils::pg_rows_to_server_records::<
48            GenAIEvalWorkflowResult,
49        >(rows, record_type),
50        _ => Err(SqlError::InvalidRecordTypeError(record_type.to_string())),
51    }
52}
53
54#[derive(Debug)]
55pub struct QueryTimestamps {
56    /// Begin and end datetimes for querying archived data
57    pub archived_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
58
59    /// Total minutes in the archived range
60    pub archived_minutes: Option<i32>,
61
62    /// Begin and end datetimes for querying active/current data
63    pub active_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
64}
65
66/// Splits a date range into archived and current table queries based on retention period
67///
68/// # Arguments
69/// * `start_datetime` - Start of the query range
70/// * `end_datetime` - End of the query range
71/// * `retention_period` - Number of days to keep data in current table
72///
73/// # Returns
74/// * `QueryTimestamps` containing:
75///   - archived_range: Some((begin, end)) if query needs archived data
76///   - archived_minutes: Some(minutes) total minutes in archived range
77///   - active_range: Some((begin, end)) if query needs current/active data
78///
79/// # Examples
80/// ```
81/// let begin = Utc::now() - Duration::days(60);  // 60 days ago
82/// let end = Utc::now() - Duration::days(1);     // yesterday
83/// let retention = 30;                           // keep 30 days in current table
84///
85/// let result = split_custom_interval(begin, end, &retention)?;
86/// // Will return:
87/// // - archived_range: Some((60 days ago, 30 days ago))
88/// // - archived_minutes: Some(43200) // minutes for 30 days
89/// // - active_range: Some((30 days ago, yesterday))
90/// ```
91pub fn split_custom_interval(
92    start_datetime: DateTime<Utc>,
93    end_datetime: DateTime<Utc>,
94    retention_period: &i32,
95) -> Result<QueryTimestamps, SqlError> {
96    if start_datetime >= end_datetime {
97        return Err(SqlError::InvalidDateRangeError);
98    }
99
100    let retention_date = Utc::now() - chrono::Duration::days(*retention_period as i64);
101    let mut timestamps = QueryTimestamps {
102        archived_range: None,
103        archived_minutes: None,
104        active_range: None,
105    };
106
107    // Handle data in archived range (before retention date)
108    if start_datetime < retention_date {
109        let archive_end = if end_datetime <= retention_date {
110            end_datetime
111        } else {
112            retention_date
113        };
114        timestamps.archived_range = Some((start_datetime, archive_end));
115        timestamps.archived_minutes = Some(
116            archive_end
117                .signed_duration_since(start_datetime)
118                .num_minutes() as i32,
119        );
120    }
121
122    // Handle data in active range (after retention date)
123    if end_datetime > retention_date {
124        let active_begin = if start_datetime < retention_date {
125            retention_date
126        } else {
127            start_datetime
128        };
129        timestamps.active_range = Some((active_begin, end_datetime));
130    }
131
132    Ok(timestamps)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use chrono::Duration;
139
140    #[test]
141    fn test_split_custom_interval() {
142        let now = Utc::now();
143        let retention_period = &30; // 30 days retention
144
145        // Case 1: Query entirely within archived range
146        let result = split_custom_interval(
147            now - Duration::days(60),
148            now - Duration::days(40),
149            retention_period,
150        )
151        .unwrap();
152        assert!(result.archived_range.is_some());
153        assert!(result.active_range.is_none());
154
155        // Case 2: Query entirely within current range
156        let result = split_custom_interval(
157            now - Duration::days(20),
158            now - Duration::days(1),
159            retention_period,
160        )
161        .unwrap();
162        assert!(result.archived_range.is_none());
163        assert!(result.active_range.is_some());
164
165        // Case 3: Query spanning both ranges
166        let result = split_custom_interval(
167            now - Duration::days(60),
168            now - Duration::days(1),
169            retention_period,
170        )
171        .unwrap();
172        assert!(result.archived_range.is_some());
173        assert!(result.active_range.is_some());
174
175        // Case 4: Invalid date range
176        let result = split_custom_interval(
177            now - Duration::days(1),
178            now - Duration::days(2),
179            retention_period,
180        );
181        assert!(result.is_err());
182    }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Hash)]
186pub struct EntityBytea(pub [u8; 16]);
187
188impl EntityBytea {
189    pub fn from_uuid(uid_str: &str) -> Result<Self, SqlError> {
190        let uuid = Uuid::from_str(uid_str)?;
191        Ok(Self(*uuid.as_bytes()))
192    }
193
194    pub fn as_bytes(&self) -> &[u8; 16] {
195        &self.0
196    }
197}