1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// SPDX-License-Identifier: BUSL-1.1
//! Protocol-neutral `ALTER SYSTEM SET <field> = <value>` handler.
//!
//! System-level settings that change at runtime without a node
//! restart. The live cell lives on `SharedState::retention_settings`;
//! the collection-GC sweeper reads it on every tick.
//!
//! Currently supported fields:
//! - `deactivated_collection_retention_days` (u32)
//!
//! Ported from the pgwire `ddl::system_ddl` handler; the superuser gate,
//! token parsing, retention-settings write, and audit side effect are
//! preserved verbatim. Only the result construction changed from pgwire
//! `Response` / `Tag` to the protocol-neutral [`DdlResult`]; the SQLSTATE codes
//! and messages are unchanged.
use crate::control::security::audit::AuditEvent;
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::state::SharedState;
use super::super::result::{DdlError, DdlResult};
/// Construct a [`DdlError`], preserving the exact SQLSTATE codes and messages
/// the pgwire handler produced.
fn err(sqlstate: &str, message: impl Into<String>) -> DdlError {
DdlError {
sqlstate: sqlstate.to_string(),
message: message.into(),
}
}
pub fn alter_system(
state: &SharedState,
identity: &AuthenticatedIdentity,
parts: &[&str],
) -> Result<Vec<DdlResult>, DdlError> {
if !identity.is_superuser {
return Err(err(
"42501",
"permission denied: only superuser can ALTER SYSTEM",
));
}
// Expected token shape:
// [0]=ALTER [1]=SYSTEM [2]=SET [3]=<field> [4]== [5]=<value>
// or without the `=` token:
// [0]=ALTER [1]=SYSTEM [2]=SET [3]=<field> [4]=<value>
if parts.len() < 5 || !parts[2].eq_ignore_ascii_case("SET") {
return Err(err("42601", "syntax: ALTER SYSTEM SET <field> = <value>"));
}
let field = parts[3].trim_end_matches(';').to_lowercase();
let value_idx = if parts.len() > 5 && parts[4] == "=" {
5
} else {
4
};
if value_idx >= parts.len() {
return Err(err("42601", "expected value after field name"));
}
let raw_value = parts[value_idx].trim_end_matches(';');
match field.as_str() {
"deactivated_collection_retention_days" => {
let days: u32 = raw_value.parse().map_err(|_| {
err(
"42601",
"deactivated_collection_retention_days must be a non-negative integer",
)
})?;
let mut w = state
.retention_settings
.write()
.map_err(|_| err("58000", "retention settings lock poisoned"))?;
w.deactivated_collection_retention_days = days;
drop(w);
state.audit_record(
AuditEvent::AdminAction,
None,
&identity.username,
&format!("altered system: set {field} = {days}"),
);
Ok(vec![DdlResult::Status {
command: "ALTER SYSTEM".to_string(),
rows_affected: None,
}])
}
other => Err(err(
"42601",
format!(
"unknown ALTER SYSTEM field: {other}. Valid: deactivated_collection_retention_days"
),
)),
}
}
#[cfg(test)]
mod tests {
//! Integration-style coverage lives in
//! `tests/collection_gc_retention.rs` — the ALTER SYSTEM path is
//! exercised end-to-end there (propose → sweeper picks up new
//! window on the next tick). The in-file suite would need a full
//! `SharedState` fixture which is heavier than necessary for the
//! parsing logic.
}