databuild/databuild/event_log/sqlite.rs
soaxelbrooke bfec05e065
Some checks are pending
/ setup (push) Waiting to run
Big change
2025-07-13 21:18:15 -07:00

821 lines
No EOL
35 KiB
Rust

use super::*;
use async_trait::async_trait;
use rusqlite::{params, Connection, Row};
use serde_json;
use std::sync::{Arc, Mutex};
// Helper functions to convert integer values back to enum values
fn int_to_build_request_status(i: i32) -> BuildRequestStatus {
match i {
0 => BuildRequestStatus::BuildRequestUnknown,
1 => BuildRequestStatus::BuildRequestReceived,
2 => BuildRequestStatus::BuildRequestPlanning,
3 => BuildRequestStatus::BuildRequestExecuting,
4 => BuildRequestStatus::BuildRequestCompleted,
5 => BuildRequestStatus::BuildRequestFailed,
6 => BuildRequestStatus::BuildRequestCancelled,
_ => BuildRequestStatus::BuildRequestUnknown,
}
}
fn int_to_partition_status(i: i32) -> PartitionStatus {
match i {
0 => PartitionStatus::PartitionUnknown,
1 => PartitionStatus::PartitionRequested,
2 => PartitionStatus::PartitionAnalyzed,
3 => PartitionStatus::PartitionBuilding,
4 => PartitionStatus::PartitionAvailable,
5 => PartitionStatus::PartitionFailed,
6 => PartitionStatus::PartitionDelegated,
_ => PartitionStatus::PartitionUnknown,
}
}
pub struct SqliteBuildEventLog {
connection: Arc<Mutex<Connection>>,
}
impl SqliteBuildEventLog {
pub async fn new(path: &str) -> Result<Self> {
let conn = Connection::open(path)
.map_err(|e| BuildEventLogError::ConnectionError(e.to_string()))?;
Ok(Self {
connection: Arc::new(Mutex::new(conn)),
})
}
// Proper event reconstruction from joined query results
fn row_to_build_event_from_join(row: &Row) -> rusqlite::Result<BuildEvent> {
let event_id: String = row.get(0)?;
let timestamp: i64 = row.get(1)?;
let build_request_id: String = row.get(2)?;
let event_type_name: String = row.get(3)?;
// Read the actual event data from the joined columns
let event_type = match event_type_name.as_str() {
"build_request" => {
// Read from build_request_events columns (indices 4, 5, 6)
let status_str: String = row.get(4)?;
let requested_partitions_json: String = row.get(5)?;
let message: String = row.get(6)?;
let status = status_str.parse::<i32>().unwrap_or(0);
let requested_partitions: Vec<PartitionRef> = serde_json::from_str(&requested_partitions_json)
.unwrap_or_default();
Some(crate::build_event::EventType::BuildRequestEvent(BuildRequestEvent {
status,
requested_partitions,
message,
}))
}
"partition" => {
// Read from partition_events columns (indices 4, 5, 6, 7)
let partition_ref: String = row.get(4)?;
let status_str: String = row.get(5)?;
let message: String = row.get(6)?;
let job_run_id: String = row.get(7).unwrap_or_default();
let status = status_str.parse::<i32>().unwrap_or(0);
Some(crate::build_event::EventType::PartitionEvent(PartitionEvent {
partition_ref: Some(PartitionRef { str: partition_ref }),
status,
message,
job_run_id,
}))
}
"job" => {
// Read from job_events columns (indices 4-10)
let job_run_id: String = row.get(4)?;
let job_label: String = row.get(5)?;
let target_partitions_json: String = row.get(6)?;
let status_str: String = row.get(7)?;
let message: String = row.get(8)?;
let config_json: Option<String> = row.get(9).ok();
let manifests_json: String = row.get(10)?;
let status = status_str.parse::<i32>().unwrap_or(0);
let target_partitions: Vec<PartitionRef> = serde_json::from_str(&target_partitions_json)
.unwrap_or_default();
let config: Option<JobConfig> = config_json
.and_then(|json| serde_json::from_str(&json).ok());
let manifests: Vec<PartitionManifest> = serde_json::from_str(&manifests_json)
.unwrap_or_default();
Some(crate::build_event::EventType::JobEvent(JobEvent {
job_run_id,
job_label: Some(JobLabel { label: job_label }),
target_partitions,
status,
message,
config,
manifests,
}))
}
"delegation" => {
// Read from delegation_events columns (indices 4, 5, 6)
let partition_ref: String = row.get(4)?;
let delegated_to_build_request_id: String = row.get(5)?;
let message: String = row.get(6)?;
Some(crate::build_event::EventType::DelegationEvent(DelegationEvent {
partition_ref: Some(PartitionRef { str: partition_ref }),
delegated_to_build_request_id,
message,
}))
}
_ => None,
};
Ok(BuildEvent {
event_id,
timestamp,
build_request_id,
event_type,
})
}
}
#[async_trait]
impl BuildEventLog for SqliteBuildEventLog {
async fn append_event(&self, event: BuildEvent) -> Result<()> {
let conn = self.connection.lock().unwrap();
// First insert into build_events table
conn.execute(
"INSERT INTO build_events (event_id, timestamp, build_request_id, event_type) VALUES (?1, ?2, ?3, ?4)",
params![
event.event_id,
event.timestamp,
event.build_request_id,
match &event.event_type {
Some(crate::build_event::EventType::BuildRequestEvent(_)) => "build_request",
Some(crate::build_event::EventType::PartitionEvent(_)) => "partition",
Some(crate::build_event::EventType::JobEvent(_)) => "job",
Some(crate::build_event::EventType::DelegationEvent(_)) => "delegation",
None => "unknown",
}
],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
// Insert into specific event type table
match &event.event_type {
Some(crate::build_event::EventType::BuildRequestEvent(br_event)) => {
let partitions_json = serde_json::to_string(&br_event.requested_partitions)
.map_err(|e| BuildEventLogError::SerializationError(e.to_string()))?;
conn.execute(
"INSERT INTO build_request_events (event_id, status, requested_partitions, message) VALUES (?1, ?2, ?3, ?4)",
params![
event.event_id,
br_event.status.to_string(),
partitions_json,
br_event.message
],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
}
Some(crate::build_event::EventType::PartitionEvent(p_event)) => {
conn.execute(
"INSERT INTO partition_events (event_id, partition_ref, status, message, job_run_id) VALUES (?1, ?2, ?3, ?4, ?5)",
params![
event.event_id,
p_event.partition_ref.as_ref().map(|r| &r.str).unwrap_or(&String::new()),
p_event.status.to_string(),
p_event.message,
if p_event.job_run_id.is_empty() { None } else { Some(&p_event.job_run_id) }
],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
}
Some(crate::build_event::EventType::JobEvent(j_event)) => {
let partitions_json = serde_json::to_string(&j_event.target_partitions)
.map_err(|e| BuildEventLogError::SerializationError(e.to_string()))?;
let config_json = j_event.config.as_ref()
.map(|c| serde_json::to_string(c))
.transpose()
.map_err(|e| BuildEventLogError::SerializationError(e.to_string()))?;
let manifests_json = serde_json::to_string(&j_event.manifests)
.map_err(|e| BuildEventLogError::SerializationError(e.to_string()))?;
conn.execute(
"INSERT INTO job_events (event_id, job_run_id, job_label, target_partitions, status, message, config_json, manifests_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
params![
event.event_id,
j_event.job_run_id,
j_event.job_label.as_ref().map(|l| &l.label).unwrap_or(&String::new()),
partitions_json,
j_event.status.to_string(),
j_event.message,
config_json,
manifests_json
],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
}
Some(crate::build_event::EventType::DelegationEvent(d_event)) => {
conn.execute(
"INSERT INTO delegation_events (event_id, partition_ref, delegated_to_build_request_id, message) VALUES (?1, ?2, ?3, ?4)",
params![
event.event_id,
d_event.partition_ref.as_ref().map(|r| &r.str).unwrap_or(&String::new()),
d_event.delegated_to_build_request_id,
d_event.message
],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
}
None => {}
}
Ok(())
}
async fn get_build_request_events(
&self,
build_request_id: &str,
since: Option<i64>
) -> Result<Vec<BuildEvent>> {
let conn = self.connection.lock().unwrap();
// Use a UNION query to get all event types with their specific data
let base_query = "
SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type,
bre.status, bre.requested_partitions, bre.message, NULL, NULL, NULL, NULL
FROM build_events be
LEFT JOIN build_request_events bre ON be.event_id = bre.event_id
WHERE be.build_request_id = ? AND be.event_type = 'build_request'
UNION ALL
SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type,
pe.partition_ref, pe.status, pe.message, pe.job_run_id, NULL, NULL, NULL
FROM build_events be
LEFT JOIN partition_events pe ON be.event_id = pe.event_id
WHERE be.build_request_id = ? AND be.event_type = 'partition'
UNION ALL
SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type,
je.job_run_id, je.job_label, je.target_partitions, je.status, je.message, je.config_json, je.manifests_json
FROM build_events be
LEFT JOIN job_events je ON be.event_id = je.event_id
WHERE be.build_request_id = ? AND be.event_type = 'job'
UNION ALL
SELECT be.event_id, be.timestamp, be.build_request_id, be.event_type,
de.partition_ref, de.delegated_to_build_request_id, de.message, NULL, NULL, NULL, NULL
FROM build_events be
LEFT JOIN delegation_events de ON be.event_id = de.event_id
WHERE be.build_request_id = ? AND be.event_type = 'delegation'
";
let query = if since.is_some() {
format!("{} AND be.timestamp > ? ORDER BY be.timestamp", base_query)
} else {
format!("{} ORDER BY be.timestamp", base_query)
};
let mut stmt = conn.prepare(&query)
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let rows = if let Some(since_timestamp) = since {
// We need 5 parameters: build_request_id for each UNION + since_timestamp
stmt.query_map(params![build_request_id, build_request_id, build_request_id, build_request_id, since_timestamp], Self::row_to_build_event_from_join)
} else {
// We need 4 parameters: build_request_id for each UNION
stmt.query_map(params![build_request_id, build_request_id, build_request_id, build_request_id], Self::row_to_build_event_from_join)
}.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let mut events = Vec::new();
for row in rows {
events.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
}
Ok(events)
}
async fn get_partition_events(
&self,
partition_ref: &str,
since: Option<i64>
) -> Result<Vec<BuildEvent>> {
// First get the build request IDs (release the connection lock quickly)
let build_ids: Vec<String> = {
let conn = self.connection.lock().unwrap();
// Get all events for builds that included this partition
// First find all build request IDs that have events for this partition
let build_ids_query = if since.is_some() {
"SELECT DISTINCT be.build_request_id
FROM build_events be
JOIN partition_events pe ON be.event_id = pe.event_id
WHERE pe.partition_ref = ? AND be.timestamp > ?"
} else {
"SELECT DISTINCT be.build_request_id
FROM build_events be
JOIN partition_events pe ON be.event_id = pe.event_id
WHERE pe.partition_ref = ?"
};
let mut stmt = conn.prepare(build_ids_query)
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let row_mapper = |row: &Row| -> rusqlite::Result<String> {
Ok(row.get::<_, String>(0)?)
};
let build_ids_result: Vec<String> = if let Some(since_timestamp) = since {
stmt.query_map(params![partition_ref, since_timestamp], row_mapper)
} else {
stmt.query_map(params![partition_ref], row_mapper)
}.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
build_ids_result
}; // Connection lock is released here
// Now get all events for those build requests (this gives us complete event reconstruction)
let mut all_events = Vec::new();
for build_id in build_ids {
let events = self.get_build_request_events(&build_id, since).await?;
all_events.extend(events);
}
// Sort events by timestamp
all_events.sort_by_key(|e| e.timestamp);
Ok(all_events)
}
async fn get_job_run_events(
&self,
_job_run_id: &str
) -> Result<Vec<BuildEvent>> {
// This method is not implemented because it would require complex joins
// to reconstruct complete event data. Use get_build_request_events instead
// which properly reconstructs all event types for a build request.
Err(BuildEventLogError::QueryError(
"get_job_run_events is not implemented - use get_build_request_events to get complete event data".to_string()
))
}
async fn get_events_in_range(
&self,
_start_time: i64,
_end_time: i64
) -> Result<Vec<BuildEvent>> {
// This method is not implemented because it would require complex joins
// to reconstruct complete event data. Use get_build_request_events instead
// which properly reconstructs all event types for a build request.
Err(BuildEventLogError::QueryError(
"get_events_in_range is not implemented - use get_build_request_events to get complete event data".to_string()
))
}
async fn execute_query(&self, query: &str) -> Result<QueryResult> {
let conn = self.connection.lock().unwrap();
let mut stmt = conn.prepare(query)
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let column_count = stmt.column_count();
let columns: Vec<String> = (0..column_count)
.map(|i| stmt.column_name(i).unwrap_or("unknown").to_string())
.collect();
let rows = stmt.query_map([], |row| {
let mut row_data = Vec::new();
for i in 0..column_count {
// Try to get as different types and convert to string
let value: String = if let Ok(int_val) = row.get::<_, i64>(i) {
int_val.to_string()
} else if let Ok(float_val) = row.get::<_, f64>(i) {
float_val.to_string()
} else if let Ok(str_val) = row.get::<_, String>(i) {
str_val
} else if let Ok(str_val) = row.get::<_, Option<String>>(i) {
str_val.unwrap_or_default()
} else {
String::new()
};
row_data.push(value);
}
Ok(row_data)
}).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let mut result_rows = Vec::new();
for row in rows {
result_rows.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
}
Ok(QueryResult {
columns,
rows: result_rows,
})
}
async fn get_latest_partition_status(
&self,
partition_ref: &str
) -> Result<Option<(PartitionStatus, i64)>> {
match self.get_meaningful_partition_status(partition_ref).await? {
Some((status, timestamp, _build_request_id)) => Ok(Some((status, timestamp))),
None => Ok(None),
}
}
async fn get_active_builds_for_partition(
&self,
partition_ref: &str
) -> Result<Vec<String>> {
let conn = self.connection.lock().unwrap();
// Look for build requests that are actively building this partition
// A build is considered active if:
// 1. It has scheduled/building events for this partition, AND
// 2. The build request itself has not completed (status 4=COMPLETED or 5=FAILED)
let query = "SELECT DISTINCT be.build_request_id
FROM partition_events pe
JOIN build_events be ON pe.event_id = be.event_id
WHERE pe.partition_ref = ?1
AND pe.status IN ('2', '3') -- PARTITION_ANALYZED or PARTITION_BUILDING
AND be.build_request_id NOT IN (
SELECT DISTINCT be3.build_request_id
FROM build_request_events bre
JOIN build_events be3 ON bre.event_id = be3.event_id
WHERE bre.status IN ('4', '5') -- BUILD_REQUEST_COMPLETED or BUILD_REQUEST_FAILED
)";
let mut stmt = conn.prepare(query)
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let rows = stmt.query_map([partition_ref], |row| {
let build_request_id: String = row.get(0)?;
Ok(build_request_id)
}).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let mut build_request_ids = Vec::new();
for row in rows {
build_request_ids.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
}
Ok(build_request_ids)
}
async fn list_build_requests(
&self,
limit: u32,
offset: u32,
status_filter: Option<BuildRequestStatus>,
) -> Result<(Vec<BuildRequestSummary>, u32)> {
let conn = self.connection.lock().unwrap();
// Build query based on status filter
let (where_clause, count_where_clause) = match status_filter {
Some(_) => (" WHERE bre.status = ?1", " WHERE bre.status = ?1"),
None => ("", ""),
};
let query = format!(
"SELECT DISTINCT be.build_request_id, bre.status, bre.requested_partitions,
MIN(be.timestamp) as created_at, MAX(be.timestamp) as updated_at
FROM build_events be
JOIN build_request_events bre ON be.event_id = bre.event_id{}
GROUP BY be.build_request_id
ORDER BY created_at DESC
LIMIT {} OFFSET {}",
where_clause, limit, offset
);
let count_query = format!(
"SELECT COUNT(DISTINCT be.build_request_id)
FROM build_events be
JOIN build_request_events bre ON be.event_id = bre.event_id{}",
count_where_clause
);
// Execute count query first
let total_count: u32 = if let Some(status) = status_filter {
let status_str = format!("{:?}", status);
conn.query_row(&count_query, params![status_str], |row| row.get(0))
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?
} else {
conn.query_row(&count_query, [], |row| row.get(0))
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?
};
// Execute main query
let mut stmt = conn.prepare(&query)
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let build_row_mapper = |row: &Row| -> rusqlite::Result<BuildRequestSummary> {
let status_str: String = row.get(1)?;
let status = status_str.parse::<i32>()
.map(int_to_build_request_status)
.unwrap_or(BuildRequestStatus::BuildRequestUnknown);
Ok(BuildRequestSummary {
build_request_id: row.get(0)?,
status,
requested_partitions: serde_json::from_str(&row.get::<_, String>(2)?).unwrap_or_default(),
created_at: row.get(3)?,
updated_at: row.get(4)?,
})
};
let rows = if let Some(status) = status_filter {
let status_str = format!("{:?}", status);
stmt.query_map(params![status_str], build_row_mapper)
} else {
stmt.query_map([], build_row_mapper)
}.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let mut summaries = Vec::new();
for row in rows {
summaries.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
}
Ok((summaries, total_count))
}
async fn list_recent_partitions(
&self,
limit: u32,
offset: u32,
status_filter: Option<PartitionStatus>,
) -> Result<(Vec<PartitionSummary>, u32)> {
// Get all unique partition refs first, ordered by most recent activity
let (total_count, partition_refs) = {
let conn = self.connection.lock().unwrap();
let count_query = "SELECT COUNT(DISTINCT pe.partition_ref)
FROM partition_events pe";
let total_count: u32 = conn.query_row(count_query, [], |row| row.get(0))
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let refs_query = "SELECT DISTINCT pe.partition_ref
FROM partition_events pe
JOIN build_events be ON pe.event_id = be.event_id
GROUP BY pe.partition_ref
ORDER BY MAX(be.timestamp) DESC
LIMIT ? OFFSET ?";
let mut stmt = conn.prepare(refs_query)
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let rows = stmt.query_map([limit, offset], |row| {
let partition_ref: String = row.get(0)?;
Ok(partition_ref)
}).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let mut partition_refs = Vec::new();
for row in rows {
partition_refs.push(row.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?);
}
(total_count, partition_refs)
};
// Get meaningful status for each partition using shared helper
let mut summaries = Vec::new();
for partition_ref in partition_refs {
if let Some((status, updated_at, build_request_id)) = self.get_meaningful_partition_status(&partition_ref).await? {
// Apply status filter if specified
if let Some(filter_status) = status_filter {
if status != filter_status {
continue;
}
}
summaries.push(PartitionSummary {
partition_ref,
status,
updated_at,
build_request_id: Some(build_request_id),
});
}
}
// Sort by updated_at descending (most recent first)
summaries.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
Ok((summaries, total_count))
}
async fn get_activity_summary(&self) -> Result<ActivitySummary> {
// First get the simple counts without holding the lock across awaits
let (active_builds_count, total_partitions_count) = {
let conn = self.connection.lock().unwrap();
// Get active builds count (builds that are not completed, failed, or cancelled)
let active_builds_count: u32 = conn.query_row(
"SELECT COUNT(DISTINCT be.build_request_id)
FROM build_events be
JOIN build_request_events bre ON be.event_id = bre.event_id
WHERE bre.status IN ('BuildRequestReceived', 'BuildRequestPlanning', 'BuildRequestExecuting')",
[],
|row| row.get(0)
).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
// Get total partitions count
let total_partitions_count: u32 = conn.query_row(
"SELECT COUNT(DISTINCT pe.partition_ref)
FROM partition_events pe
JOIN build_events be ON pe.event_id = be.event_id",
[],
|row| row.get(0)
).map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
(active_builds_count, total_partitions_count)
};
// Get recent builds (limit to 5 for summary)
let (recent_builds, _) = self.list_build_requests(5, 0, None).await?;
// Get recent partitions (limit to 5 for summary)
let (recent_partitions, _) = self.list_recent_partitions(5, 0, None).await?;
Ok(ActivitySummary {
active_builds_count,
recent_builds,
recent_partitions,
total_partitions_count,
})
}
async fn initialize(&self) -> Result<()> {
let conn = self.connection.lock().unwrap();
// Create tables
conn.execute(
"CREATE TABLE IF NOT EXISTS build_events (
event_id TEXT PRIMARY KEY,
timestamp INTEGER NOT NULL,
build_request_id TEXT NOT NULL,
event_type TEXT NOT NULL
)",
[],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
conn.execute(
"CREATE TABLE IF NOT EXISTS build_request_events (
event_id TEXT PRIMARY KEY REFERENCES build_events(event_id),
status TEXT NOT NULL,
requested_partitions TEXT NOT NULL,
message TEXT
)",
[],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
conn.execute(
"CREATE TABLE IF NOT EXISTS partition_events (
event_id TEXT PRIMARY KEY REFERENCES build_events(event_id),
partition_ref TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
job_run_id TEXT
)",
[],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
conn.execute(
"CREATE TABLE IF NOT EXISTS job_events (
event_id TEXT PRIMARY KEY REFERENCES build_events(event_id),
job_run_id TEXT NOT NULL,
job_label TEXT NOT NULL,
target_partitions TEXT NOT NULL,
status TEXT NOT NULL,
message TEXT,
config_json TEXT,
manifests_json TEXT
)",
[],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
conn.execute(
"CREATE TABLE IF NOT EXISTS delegation_events (
event_id TEXT PRIMARY KEY REFERENCES build_events(event_id),
partition_ref TEXT NOT NULL,
delegated_to_build_request_id TEXT NOT NULL,
message TEXT
)",
[],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
// Create indexes
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_build_events_build_request ON build_events(build_request_id, timestamp)",
[],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_build_events_timestamp ON build_events(timestamp)",
[],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_partition_events_partition ON partition_events(partition_ref)",
[],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_job_events_job_run ON job_events(job_run_id)",
[],
).map_err(|e| BuildEventLogError::DatabaseError(e.to_string()))?;
Ok(())
}
async fn get_build_request_for_available_partition(
&self,
partition_ref: &str
) -> Result<Option<String>> {
let conn = self.connection.lock().unwrap();
// Find the most recent PARTITION_AVAILABLE event for this partition
let query = "SELECT be.build_request_id
FROM partition_events pe
JOIN build_events be ON pe.event_id = be.event_id
WHERE pe.partition_ref = ?1 AND pe.status = '4'
ORDER BY be.timestamp DESC
LIMIT 1";
let result = conn.query_row(query, [partition_ref], |row| {
let build_request_id: String = row.get(0)?;
Ok(build_request_id)
});
match result {
Ok(build_request_id) => Ok(Some(build_request_id)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(BuildEventLogError::QueryError(e.to_string())),
}
}
}
impl SqliteBuildEventLog {
// Shared helper method to get the meaningful partition status for build coordination and display
// This implements the "delegation-friendly" logic: if a partition was ever available, it remains available
async fn get_meaningful_partition_status(
&self,
partition_ref: &str
) -> Result<Option<(PartitionStatus, i64, String)>> { // (status, timestamp, build_request_id)
let conn = self.connection.lock().unwrap();
// Check for ANY historical completion first - this is resilient to later events being added
let available_query = "SELECT pe.status, be.timestamp, be.build_request_id
FROM partition_events pe
JOIN build_events be ON pe.event_id = be.event_id
WHERE pe.partition_ref = ?1 AND pe.status = '4'
ORDER BY be.timestamp DESC
LIMIT 1";
let mut available_stmt = conn.prepare(available_query)
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let available_result = available_stmt.query_row([partition_ref], |row| {
let status_str: String = row.get(0)?;
let timestamp: i64 = row.get(1)?;
let build_request_id: String = row.get(2)?;
let status = status_str.parse::<i32>()
.map_err(|_e| rusqlite::Error::InvalidColumnType(0, status_str.clone(), rusqlite::types::Type::Integer))?;
Ok((status, timestamp, build_request_id))
});
match available_result {
Ok((status, timestamp, build_request_id)) => {
let partition_status = PartitionStatus::try_from(status)
.map_err(|_| BuildEventLogError::QueryError(format!("Invalid partition status: {}", status)))?;
return Ok(Some((partition_status, timestamp, build_request_id)));
}
Err(rusqlite::Error::QueryReturnedNoRows) => {
// No available partition found, fall back to latest status
}
Err(e) => return Err(BuildEventLogError::QueryError(e.to_string())),
}
// Fall back to latest status if no available partition found
let latest_query = "SELECT pe.status, be.timestamp, be.build_request_id
FROM partition_events pe
JOIN build_events be ON pe.event_id = be.event_id
WHERE pe.partition_ref = ?1
ORDER BY be.timestamp DESC
LIMIT 1";
let mut latest_stmt = conn.prepare(latest_query)
.map_err(|e| BuildEventLogError::QueryError(e.to_string()))?;
let result = latest_stmt.query_row([partition_ref], |row| {
let status_str: String = row.get(0)?;
let timestamp: i64 = row.get(1)?;
let build_request_id: String = row.get(2)?;
let status = status_str.parse::<i32>()
.map_err(|_e| rusqlite::Error::InvalidColumnType(0, status_str.clone(), rusqlite::types::Type::Integer))?;
Ok((status, timestamp, build_request_id))
});
match result {
Ok((status, timestamp, build_request_id)) => {
let partition_status = PartitionStatus::try_from(status)
.map_err(|_| BuildEventLogError::QueryError(format!("Invalid partition status: {}", status)))?;
Ok(Some((partition_status, timestamp, build_request_id)))
}
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(BuildEventLogError::QueryError(e.to_string())),
}
}
}