scouter_sql/sql/
utils.rs

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