scouter_sql/sql/
utils.rs

1use crate::sql::error::SqlError;
2use crate::sql::schema::llm_drift_record_from_row;
3use chrono::{DateTime, Utc};
4use scouter_types::{
5    CustomMetricServerRecord, LLMMetricRecord, PsiServerRecord, RecordType, ServerRecord,
6    ServerRecords, SpcServerRecord,
7};
8
9use sqlx::{postgres::PgRow, Row};
10/// Helper for converting a row to an `SpcServerRecord`.
11fn spc_record_from_row(row: &PgRow) -> Result<SpcServerRecord, SqlError> {
12    Ok(SpcServerRecord {
13        created_at: row.try_get("created_at")?,
14        name: row.try_get("name")?,
15        space: row.try_get("space")?,
16        version: row.try_get("version")?,
17        feature: row.try_get("feature")?,
18        value: row.try_get("value")?,
19    })
20}
21
22/// Helper for converting a row to a `PsiServerRecord`.
23fn psi_record_from_row(row: &PgRow) -> Result<PsiServerRecord, SqlError> {
24    let bin_id: i32 = row.try_get("bin_id")?;
25    let bin_count: i32 = row.try_get("bin_count")?;
26
27    Ok(PsiServerRecord {
28        created_at: row.try_get("created_at")?,
29        name: row.try_get("name")?,
30        space: row.try_get("space")?,
31        version: row.try_get("version")?,
32        feature: row.try_get("feature")?,
33        bin_id: bin_id as usize,
34        bin_count: bin_count as usize,
35    })
36}
37
38/// Helper for converting a row to a `ustomMetricServerRecord`.
39fn custom_record_from_row(row: &PgRow) -> Result<CustomMetricServerRecord, SqlError> {
40    Ok(CustomMetricServerRecord {
41        created_at: row.try_get("created_at")?,
42        name: row.try_get("name")?,
43        space: row.try_get("space")?,
44        version: row.try_get("version")?,
45        metric: row.try_get("metric")?,
46        value: row.try_get("value")?,
47    })
48}
49
50fn llm_drift_metric_from_row(row: &PgRow) -> Result<LLMMetricRecord, SqlError> {
51    Ok(LLMMetricRecord {
52        record_uid: row.try_get("record_uid")?,
53        created_at: row.try_get("created_at")?,
54        space: row.try_get("space")?,
55        name: row.try_get("name")?,
56        version: row.try_get("version")?,
57        metric: row.try_get("metric")?,
58        value: row.try_get("value")?,
59    })
60}
61
62/// Converts a slice of `PgRow` to a `ServerRecords` based on the provided `RecordType`.
63///
64/// # Arguments
65/// * `rows` - A slice of `PgRow` to be converted.
66/// * `record_type` - The type of record to convert to.
67///
68/// # Returns
69/// * `Result<ServerRecords, SqlError>` - A result containing the converted `ServerRecords` or an error.
70///
71/// # Errors
72/// * Returns an error if the conversion fails or if the record type is not supported.
73pub fn pg_rows_to_server_records(
74    rows: &[PgRow],
75    record_type: &RecordType,
76) -> Result<ServerRecords, SqlError> {
77    // Get correct conversion function base on record type
78    // Returns an error if the record type is not supported
79    let convert_fn = match record_type {
80        RecordType::Spc => |row| Ok(ServerRecord::Spc(spc_record_from_row(row)?)),
81        RecordType::Psi => |row| Ok(ServerRecord::Psi(psi_record_from_row(row)?)),
82        RecordType::Custom => |row| Ok(ServerRecord::Custom(custom_record_from_row(row)?)),
83        RecordType::LLMDrift => |row| Ok(ServerRecord::LLMDrift(llm_drift_record_from_row(row)?)),
84        RecordType::LLMMetric => |row| Ok(ServerRecord::LLMMetric(llm_drift_metric_from_row(row)?)),
85        _ => return Err(SqlError::InvalidRecordTypeError(record_type.to_string())),
86    };
87
88    // Pre-allocate vector with exact capacity needed
89    let records: Result<Vec<ServerRecord>, SqlError> = rows.iter().map(convert_fn).collect();
90
91    // Convert the result into ServerRecords
92    records.map(ServerRecords::new)
93}
94
95#[derive(Debug)]
96pub struct QueryTimestamps {
97    /// Begin and end datetimes for querying archived data
98    pub archived_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
99
100    pub archived_minutes: Option<i32>,
101
102    /// Minutes from retention date to end_datetime for querying current data
103    pub current_minutes: Option<i32>,
104}
105
106/// Splits a date range into archived and current table queries based on retention period
107///
108/// # Arguments
109/// * `begin_datetime` - Start of the query range
110/// * `end_datetime` - End of the query range
111/// * `retention_period` - Number of days to keep data in current table
112///
113/// # Returns
114/// * `QueryTimestamps` containing:
115///   - archived_range: Some((begin, end)) if query needs archived data
116///   - current_minutes: Some(minutes) if query needs current data
117///
118/// # Examples
119/// ```
120/// let begin = Utc::now() - Duration::days(60);  // 60 days ago
121/// let end = Utc::now() - Duration::days(1);     // yesterday
122/// let retention = 30;                           // keep 30 days in current table
123///
124/// let result = split_custom_interval(begin, end, &retention)?;
125/// // Will return:
126/// // - archived_range: Some((60 days ago, 30 days ago))
127/// // - current_minutes: Some(41760) // minutes for last 29 days
128/// ```
129pub fn split_custom_interval(
130    begin_datetime: DateTime<Utc>,
131    end_datetime: DateTime<Utc>,
132    retention_period: &i32,
133) -> Result<QueryTimestamps, SqlError> {
134    if begin_datetime >= end_datetime {
135        return Err(SqlError::InvalidDateRangeError);
136    }
137
138    let retention_date = Utc::now() - chrono::Duration::days(*retention_period as i64);
139    let mut timestamps = QueryTimestamps {
140        archived_range: None,
141        current_minutes: None,
142        archived_minutes: None,
143    };
144
145    // Handle data in archived range (before retention date)
146    if begin_datetime < retention_date {
147        let archive_end = if end_datetime <= retention_date {
148            end_datetime
149        } else {
150            retention_date
151        };
152        timestamps.archived_range = Some((begin_datetime, archive_end));
153    }
154
155    // Handle data in current range (after retention date)
156    if end_datetime > retention_date {
157        let current_begin = if begin_datetime < retention_date {
158            retention_date
159        } else {
160            begin_datetime
161        };
162        let minutes = end_datetime
163            .signed_duration_since(current_begin)
164            .num_minutes() as i32;
165        timestamps.current_minutes = Some(minutes);
166    }
167
168    // calculate archived minutes
169    if let Some((begin, end)) = timestamps.archived_range {
170        timestamps.archived_minutes = Some(end.signed_duration_since(begin).num_minutes() as i32);
171    }
172
173    Ok(timestamps)
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use chrono::Duration;
180
181    #[test]
182    fn test_split_custom_interval() {
183        let now = Utc::now();
184        let retention_period = &30; // 30 days retention
185
186        // Case 1: Query entirely within archived range
187        let result = split_custom_interval(
188            now - Duration::days(60),
189            now - Duration::days(40),
190            retention_period,
191        )
192        .unwrap();
193        assert!(result.archived_range.is_some());
194        assert!(result.current_minutes.is_none());
195
196        // Case 2: Query entirely within current range
197        let result = split_custom_interval(
198            now - Duration::days(20),
199            now - Duration::days(1),
200            retention_period,
201        )
202        .unwrap();
203        assert!(result.archived_range.is_none());
204        assert!(result.current_minutes.is_some());
205
206        // Case 3: Query spanning both ranges
207        let result = split_custom_interval(
208            now - Duration::days(60),
209            now - Duration::days(1),
210            retention_period,
211        )
212        .unwrap();
213        assert!(result.archived_range.is_some());
214        assert!(result.current_minutes.is_some());
215
216        // Case 4: Invalid date range
217        let result = split_custom_interval(
218            now - Duration::days(1),
219            now - Duration::days(2),
220            retention_period,
221        );
222        assert!(result.is_err());
223    }
224}