Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/database/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,17 @@ diesel::table! {
version_known_issues (id) {
id -> Uuid,
created_at -> Timestamptz,
version_id -> Uuid,
author -> Text,
description -> Text,
resolved_at -> Nullable<Timestamptz>,
resolved_by -> Nullable<Text>,
resolution_message -> Nullable<Text>,
min_major -> Int4,
min_minor -> Int4,
min_patch -> Int4,
max_major -> Nullable<Int4>,
max_minor -> Nullable<Int4>,
max_patch -> Nullable<Int4>,
}
}

Expand Down Expand Up @@ -284,7 +289,6 @@ diesel::joinable!(slack_outbox -> incidents (incident_id));
diesel::joinable!(slack_outbox -> issues (issue_id));
diesel::joinable!(statuses -> devices (device_id));
diesel::joinable!(statuses -> servers (server_id));
diesel::joinable!(version_known_issues -> versions (version_id));
diesel::joinable!(versions -> devices (device_id));

diesel::allow_tables_to_appear_in_same_query!(
Expand Down
92 changes: 66 additions & 26 deletions crates/database/src/version_known_issues.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
//! Operator-flagged known issues attached to a version.
//! Operator-flagged known issues attached to a version range.
//!
//! Rows are append-only: instead of editing or deleting, an operator
//! resolves an issue with a `resolution_message`. A version is `ready`
//! when it has no unresolved (open) known issues.
//! Each row covers a half-open range `[min, max)` of patches within a
//! single minor branch. Raising sets `min` to the affected version and
//! leaves `max_*` NULL — the issue then implicitly covers every later
//! patch in that minor. Resolving sets `max_*` to the fix version (the
//! first unaffected patch). Resolution is append-only: instead of
//! editing, an operator records a `resolution_message` alongside the
//! fix version.

use commons_errors::{AppError, Result};
use diesel::prelude::*;
Expand All @@ -11,36 +15,40 @@ use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::versions::Version;

#[derive(Clone, Debug, Serialize, Deserialize, Queryable, Selectable, Associations)]
#[diesel(belongs_to(Version))]
#[derive(Clone, Debug, Serialize, Deserialize, Queryable, Selectable)]
#[diesel(table_name = crate::schema::version_known_issues)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct VersionKnownIssue {
pub id: Uuid,
#[diesel(deserialize_as = jiff_diesel::Timestamp, serialize_as = jiff_diesel::Timestamp)]
pub created_at: Timestamp,
pub version_id: Uuid,
pub author: String,
pub description: String,
#[diesel(deserialize_as = jiff_diesel::NullableTimestamp, serialize_as = jiff_diesel::NullableTimestamp)]
pub resolved_at: Option<Timestamp>,
pub resolved_by: Option<String>,
pub resolution_message: Option<String>,
pub min_major: i32,
pub min_minor: i32,
pub min_patch: i32,
pub max_major: Option<i32>,
pub max_minor: Option<i32>,
pub max_patch: Option<i32>,
}

impl VersionKnownIssue {
pub async fn add(
db: &mut AsyncPgConnection,
version_id: Uuid,
min: (i32, i32, i32),
author: &str,
description: &str,
) -> Result<Self> {
use crate::schema::version_known_issues;
diesel::insert_into(version_known_issues::table)
.values((
version_known_issues::version_id.eq(version_id),
version_known_issues::min_major.eq(min.0),
version_known_issues::min_minor.eq(min.1),
version_known_issues::min_patch.eq(min.2),
version_known_issues::author.eq(author),
version_known_issues::description.eq(description),
))
Expand All @@ -50,14 +58,20 @@ impl VersionKnownIssue {
.map_err(AppError::from)
}

pub async fn list_for_version(
/// All known issues whose minor branch matches the given (major, minor)
/// — ordered newest first. Used by the version-detail UI to show
/// every issue ever raised against this minor, including ones already
/// resolved on an earlier patch.
pub async fn list_for_minor(
db: &mut AsyncPgConnection,
version_id: Uuid,
major: i32,
minor: i32,
) -> Result<Vec<Self>> {
use crate::schema::version_known_issues::dsl;
dsl::version_known_issues
.select(Self::as_select())
.filter(dsl::version_id.eq(version_id))
.filter(dsl::min_major.eq(major))
.filter(dsl::min_minor.eq(minor))
.order(dsl::created_at.desc())
.load(db)
.await
Expand All @@ -67,15 +81,22 @@ impl VersionKnownIssue {
pub async fn resolve(
db: &mut AsyncPgConnection,
issue_id: Uuid,
fix: (i32, i32, i32),
resolved_by: &str,
resolution_message: &str,
) -> Result<Self> {
use crate::schema::version_known_issues::dsl;
let now = Timestamp::now();
diesel::update(dsl::version_known_issues)
.filter(dsl::id.eq(issue_id))
.filter(dsl::resolved_at.is_null())
.filter(dsl::max_major.is_null())
.filter(dsl::min_major.eq(fix.0))
.filter(dsl::min_minor.eq(fix.1))
.filter(dsl::min_patch.lt(fix.2))
.set((
dsl::max_major.eq(Some(fix.0)),
dsl::max_minor.eq(Some(fix.1)),
dsl::max_patch.eq(Some(fix.2)),
dsl::resolved_at.eq(jiff_diesel::NullableTimestamp::from(Some(now))),
dsl::resolved_by.eq(resolved_by),
dsl::resolution_message.eq(resolution_message),
Expand All @@ -86,32 +107,51 @@ impl VersionKnownIssue {
.map_err(AppError::from)
}

/// Return the set of version IDs (from `ids`) that have at least one
/// unresolved known issue. Used to compute the `ready` flag in batch.
pub async fn versions_with_open(
/// Subset of `ids` whose (major, minor, patch) is covered by any
/// known issue's range. Used to compute the `ready` flag in batch.
pub async fn affected_versions(
db: &mut AsyncPgConnection,
ids: &[Uuid],
) -> Result<std::collections::HashSet<Uuid>> {
use crate::schema::version_known_issues::dsl;
use crate::schema::{version_known_issues as k, versions as v};
if ids.is_empty() {
return Ok(std::collections::HashSet::new());
}
let rows: Vec<Uuid> = dsl::version_known_issues
.select(dsl::version_id)
.filter(dsl::version_id.eq_any(ids))
.filter(dsl::resolved_at.is_null())
let rows: Vec<Uuid> = v::table
.inner_join(
k::table.on(k::min_major
.eq(v::major)
.and(k::min_minor.eq(v::minor))
.and(v::patch.ge(k::min_patch))
.and(k::max_patch.is_null().or(v::patch.lt(k::max_patch.assume_not_null())))),
)
.select(v::id)
.filter(v::id.eq_any(ids))
.distinct()
.load(db)
.await
.map_err(AppError::from)?;
Ok(rows.into_iter().collect())
}

pub async fn version_is_ready(db: &mut AsyncPgConnection, version_id: Uuid) -> Result<bool> {
/// Whether a specific version (by coordinates) is unaffected by any
/// known issue.
pub async fn version_is_ready(
db: &mut AsyncPgConnection,
major: i32,
minor: i32,
patch: i32,
) -> Result<bool> {
use crate::schema::version_known_issues::dsl;
let count: i64 = dsl::version_known_issues
.filter(dsl::version_id.eq(version_id))
.filter(dsl::resolved_at.is_null())
.filter(dsl::min_major.eq(major))
.filter(dsl::min_minor.eq(minor))
.filter(dsl::min_patch.le(patch))
.filter(
dsl::max_patch
.is_null()
.or(dsl::max_patch.assume_not_null().gt(patch)),
)
.count()
.get_result(db)
.await
Expand Down
10 changes: 4 additions & 6 deletions crates/database/tests/reachability_sweep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,10 @@ struct RowSecs {
#[tokio::test(flavor = "multi_thread")]
async fn new_servers_default_to_ten_minutes() {
commons_tests::db::TestDb::run(async |mut conn, _| {
sql_query(
"INSERT INTO servers (host) VALUES ('http://new.invalid/')",
)
.execute(&mut conn)
.await
.expect("insert default");
sql_query("INSERT INTO servers (host) VALUES ('http://new.invalid/')")
.execute(&mut conn)
.await
.expect("insert default");

let row: RowSecs = sql_query(
r#"
Expand Down
Loading