scouter_sql/sql/
utils.rs

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