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
30 changes: 30 additions & 0 deletions crates/sqlbuild-cli/src/argv/command_table.json
Original file line number Diff line number Diff line change
Expand Up @@ -5198,6 +5198,7 @@
"nargs": 0,
"const": true,
"default": false,
"help": "skip the confirmation prompt and run the planned archives, deletes and prunes",
"group": 1
},
{
Expand All @@ -5207,6 +5208,7 @@
],
"dest": "retention_days",
"type": "int",
"help": "days a stale relation must be unchanged before it is archived (overrides janitor.retention_days)",
"group": 1
},
{
Expand All @@ -5216,6 +5218,7 @@
],
"dest": "direct_state_history_versions",
"type": "int",
"help": "state history rows to keep per identity when pruning direct state tables (overrides janitor.direct_state_history_versions)",
"group": 1
},
{
Expand All @@ -5226,6 +5229,32 @@
"dest": "drop_old_name_views",
"default": [],
"metavar": "NAME",
"help": "drop the compatibility view at this old model name before it expires; repeat for several views",
"group": 1
},
{
"kind": "store_true",
"options": [
"--verbose",
"-v"
],
"dest": "verbose",
"nargs": 0,
"const": true,
"default": false,
"help": "list every entry of each preview section and every skipped object",
"group": 1
},
{
"kind": "store_true",
"options": [
"--json"
],
"dest": "json",
"nargs": 0,
"const": true,
"default": false,
"help": "print the plan and result as JSON on stdout; progress goes to stderr",
"group": 1
},
{
Expand All @@ -5234,6 +5263,7 @@
"--target"
],
"dest": "target",
"help": "clean up this configured target instead of the active target",
"group": 1
},
{
Expand Down
4 changes: 3 additions & 1 deletion crates/sqlbuild-cli/src/dispatch/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ pub(crate) const DEBUG_ROUTE: Route = routed(
},
);

const JANITOR_FIELDS: [Field; 8] = [
const JANITOR_FIELDS: [Field; 10] = [
PROJECT_DIR,
NO_COLOR,
same("auto_approve"),
Expand All @@ -496,6 +496,8 @@ const JANITOR_FIELDS: [Field; 8] = [
),
SELECTED_TARGET,
same("as_target"),
same("verbose"),
JSON_OUTPUT,
];
const JANITOR_ARGUMENTS: [Field; 1] = [request(class("JanitorCommandRequest"), &JANITOR_FIELDS)];
pub(crate) const JANITOR_ROUTE: Route = routed(
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-core/src/janitor_text/_helpers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
pub(crate) mod counts;
pub(crate) mod phases;
pub(crate) mod plan_json;
pub(crate) mod plan_sections;
47 changes: 47 additions & 0 deletions crates/sqlbuild-core/src/janitor_text/_helpers/phases.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Wording and durations of janitor apply phases.
use crate::cli_text::main::format_count_noun::format_count_noun;
use crate::janitor_text::constants::{MINUTE_SECONDS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
use crate::janitor_text::models::JanitorPhase;

/// The phase's present participle, past tense and singular noun.
pub(crate) fn phase_words(phase: JanitorPhase) -> (&'static str, &'static str, &'static str) {
match phase {
JanitorPhase::DeleteRelations => ("Deleting", "Deleted", "relation"),
JanitorPhase::Archive => ("Archiving", "Archived", "relation"),
JanitorPhase::DeleteArchives => ("Deleting", "Deleted", "expired archive"),
JanitorPhase::DropOldNameViews => ("Dropping", "Dropped", "old-name view"),
JanitorPhase::DropQueryArtifacts => ("Dropping", "Dropped", "query artifact"),
JanitorPhase::PruneDirectState => ("Pruning", "Pruned", "direct state table"),
}
}

/// `count` with the singular noun for one and its plural otherwise.
pub(crate) fn noun(count: usize, singular: &str) -> String {
format_count_noun(i64::try_from(count).unwrap_or(i64::MAX), singular, None)
}

/// Seconds with two decimals under a minute, then whole minutes and seconds.
pub(crate) fn duration(seconds: f64) -> String {
if seconds < MINUTE_SECONDS {
return format!("{:.2}s", seconds.max(0.0));
}
let whole: u64 = seconds.round() as u64;
let (hours, minutes, rest) = (
whole / SECONDS_PER_HOUR,
whole % SECONDS_PER_HOUR / SECONDS_PER_MINUTE,
whole % SECONDS_PER_MINUTE,
);
if hours > 0 {
return format!("{hours}h {minutes:02}m {rest:02}s");
}
format!("{minutes}m {rest:02}s")
}

/// `name -> destination`, or the name alone.
pub(crate) fn flow(name: &str, destination: Option<&str>) -> String {
destination.map_or_else(
|| name.to_owned(),
|destination| format!("{name} -> {destination}"),
)
}
255 changes: 255 additions & 0 deletions crates/sqlbuild-core/src/janitor_text/_helpers/plan_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
//! The JSON payload of `sqb janitor --json`.
use crate::janitor_text::constants::{DROP_EARLY, DROP_EXPIRED};
use crate::janitor_text::models::{
JanitorItemText, JanitorPlanText, JanitorResultCounts, JanitorRunText, OldNameViewText,
SourceSchemaText,
};
use crate::json::models::{JsonInteger, JsonValue};

fn text(value: &str) -> JsonValue {
JsonValue::String(value.to_owned())
}

fn optional_text(value: Option<&str>) -> JsonValue {
value.map_or(JsonValue::Null, text)
}

fn texts(values: &[String]) -> JsonValue {
JsonValue::Array(values.iter().map(|value| text(value)).collect())
}

fn integer(value: impl ToString) -> JsonValue {
JsonInteger::parse(&value.to_string()).map_or(JsonValue::Null, JsonValue::Integer)
}

fn optional_integer(value: Option<i64>) -> JsonValue {
value.map_or(JsonValue::Null, integer)
}

fn object(fields: Vec<(&str, JsonValue)>) -> JsonValue {
JsonValue::Object(
fields
.into_iter()
.map(|(key, value)| (key.to_owned(), value))
.collect(),
)
}

/// A preview `YYYY-MM-DD HH:MM:SS` UTC time as ISO 8601.
fn utc_timestamp(value: &str) -> JsonValue {
if value.is_empty() {
return JsonValue::Null;
}
text(&format!("{}Z", value.replacen(' ', "T", 1)))
}

fn old_name_view(view: &OldNameViewText, planned: &str) -> JsonValue {
let action: &str = match (planned, view.drop_reason.as_deref()) {
("drop", Some(DROP_EARLY | DROP_EXPIRED)) => "drop",
("live", _) => "keep",
_ => "record_dropped",
};
object(vec![
("name", text(&view.name)),
("destination_model", text(&view.destination_model)),
("expires_on", optional_text(view.expires_on.as_deref())),
("action", text(action)),
("drop_reason", optional_text(view.drop_reason.as_deref())),
("occupied", JsonValue::Bool(view.occupied)),
("claimed_by", optional_text(view.claimed_by.as_deref())),
])
}

fn source_schema(schema: &SourceSchemaText, blocked: bool) -> JsonValue {
let mut fields: Vec<(&str, JsonValue)> = vec![
("name", text(&schema.name)),
("source_names", texts(&schema.source_names)),
];
if blocked {
fields.push((
"suppressed_candidates",
texts(&schema.suppressed_candidates),
));
fields.push((
"suppressed_archive_deletions",
texts(&schema.suppressed_archive_deletions),
));
}
object(fields)
}

/// Every planned action, retained archive and skipped object of the plan.
pub(crate) fn plan_payload(plan: &JanitorPlanText) -> JsonValue {
let views: Vec<JsonValue> = plan
.old_name_drops
.iter()
.map(|view| old_name_view(view, "drop"))
.chain(
plan.old_name_live
.iter()
.map(|view| old_name_view(view, "live")),
)
.chain(
plan.old_name_missing
.iter()
.map(|view| old_name_view(view, "missing")),
)
.collect();
object(vec![
("direct_mode", JsonValue::Bool(plan.direct_mode)),
("retention_days", integer(plan.retention_days)),
(
"archive_retention_days",
integer(plan.archive_retention_days),
),
(
"age_metadata_supported",
JsonValue::Bool(plan.age_metadata_supported),
),
("schemas_scanned", integer(plan.scanned_schema_count)),
("eligible_for_deletion", texts(&plan.candidates)),
(
"relations_to_archive",
JsonValue::Array(
plan.archive_candidates
.iter()
.map(|candidate| {
object(vec![
("name", text(&candidate.name)),
("archive_name", text(&candidate.archive_name)),
("age_days", optional_integer(candidate.age_days)),
("expires_at", utc_timestamp(&candidate.expires_at)),
("deleted_in_run", JsonValue::Bool(candidate.expires_now)),
])
})
.collect(),
),
),
(
"archives_to_delete",
JsonValue::Array(
plan.archive_deletions
.iter()
.map(|archive| {
object(vec![
("name", text(&archive.name)),
("archived_in_run", JsonValue::Bool(archive.archived_in_run)),
("archived_at", utc_timestamp(&archive.archived_at)),
("age_days", optional_integer(archive.age_days)),
("expires_at", utc_timestamp(&archive.expires_at)),
])
})
.collect(),
),
),
(
"retained_archives",
JsonValue::Array(
plan.retained_archives
.iter()
.map(|archive| {
object(vec![
("name", text(&archive.name)),
("archived_at", utc_timestamp(&archive.archived_at)),
("expires_at", utc_timestamp(&archive.expires_at)),
])
})
.collect(),
),
),
("old_name_views", JsonValue::Array(views)),
("query_artifacts", texts(&plan.query_artifacts)),
(
"direct_state_prunes",
JsonValue::Array(
plan.state_prunes
.iter()
.map(|(name, versions)| {
object(vec![
("name", text(name)),
("retain_versions", integer(*versions)),
])
})
.collect(),
),
),
(
"skipped_objects",
JsonValue::Array(
plan.skipped_relations
.iter()
.map(|(name, reason)| {
object(vec![("name", text(name)), ("reason", text(reason))])
})
.collect(),
),
),
(
"skipped_schemas",
JsonValue::Array(
plan.skipped_schemas
.iter()
.map(|schema| source_schema(schema, false))
.collect(),
),
),
(
"blocked_schemas",
JsonValue::Array(
plan.blocked_schemas
.iter()
.map(|schema| source_schema(schema, true))
.collect(),
),
),
])
}

fn counts_payload(counts: &JanitorResultCounts) -> JsonValue {
object(vec![
("archived", integer(counts.archived)),
("deleted", integer(counts.deleted)),
("deleted_archives", integer(counts.deleted_archives)),
(
"deleted_query_artifacts",
integer(counts.deleted_query_artifacts),
),
(
"dropped_old_name_views",
integer(counts.dropped_old_name_views),
),
("pruned_direct_state", integer(counts.pruned_direct_state)),
])
}

fn item_payload(item: &JanitorItemText) -> JsonValue {
object(vec![
("phase", text(item.phase.value())),
("action", text(&item.action)),
("name", text(&item.name)),
("destination", optional_text(item.destination.as_deref())),
(
"status",
text(if item.error.is_some() { "failed" } else { "ok" }),
),
("error", optional_text(item.error.as_deref())),
(
"duration_seconds",
item.duration_seconds
.map_or(JsonValue::Null, JsonValue::Float),
),
])
}

/// The counts, actions and duration of a run.
pub(crate) fn run_payload(run: &JanitorRunText) -> JsonValue {
object(vec![
("counts", counts_payload(&run.counts)),
("elapsed_seconds", JsonValue::Float(run.elapsed_seconds)),
(
"items",
JsonValue::Array(run.items.iter().map(item_payload).collect()),
),
])
}
Loading
Loading