Skip to main content

reifydb_core/interface/catalog/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{fmt, str::FromStr};
5
6use reifydb_value::value::{Value, duration::Duration, value_type::ValueType};
7
8use crate::common::CommitVersion;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum AcceptError {
12	TypeMismatch {
13		expected: Vec<ValueType>,
14		actual: ValueType,
15	},
16
17	InvalidValue(String),
18}
19
20impl fmt::Display for AcceptError {
21	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22		match self {
23			Self::TypeMismatch {
24				expected,
25				actual,
26			} => {
27				write!(f, "expected one of {:?}, got {:?}", expected, actual)
28			}
29			Self::InvalidValue(reason) => write!(f, "{reason}"),
30		}
31	}
32}
33
34#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
35pub enum ConfigKey {
36	OracleWindowSize,
37	OracleWaterMark,
38	QueryRowBatchSize,
39	RowTtlScanBatchSize,
40	RowTtlScanInterval,
41	OperatorTtlScanBatchSize,
42	OperatorTtlScanInterval,
43	VersionEpochSampleInterval,
44	HistoricalGcBatchSize,
45	HistoricalGcInterval,
46	CdcTtlDuration,
47	CdcTtlScanInterval,
48	CdcTtlScanBatchSize,
49	CdcTtlScanMaxBatchesPerTick,
50	CdcCompactInterval,
51	CdcCompactBlockSize,
52	CdcCompactSafetyLag,
53	CdcCompactMaxBlocksPerTick,
54	CdcCompactBlockCacheCapacity,
55	CdcCompactZstdLevel,
56	CdcRecentCacheCapacity,
57	CdcWalAutocheckpoint,
58	MultiReadBufferPages,
59	MultiReadBufferPageSize,
60	MultiFlushInterval,
61	MultiWalAutocheckpoint,
62	FlowTick,
63	CdcWatermarkWaitTimeout,
64	CdcConsumeWaitTimeout,
65	FlowJoinProbeBlockSize,
66	ThreadsAsync,
67	ThreadsCoordination,
68	ThreadsFlow,
69	ThreadsTask,
70	ThreadsCompute,
71	SubscriptionWorkerThreads,
72	RuntimeMetricsInterval,
73	MetricFlushInterval,
74	MetricsRuntimeRetention,
75	MetricsProfilerRetention,
76	MetricsProfilerSnapshotInterval,
77}
78
79impl ConfigKey {
80	pub fn all() -> &'static [Self] {
81		&[
82			Self::OracleWindowSize,
83			Self::OracleWaterMark,
84			Self::QueryRowBatchSize,
85			Self::RowTtlScanBatchSize,
86			Self::RowTtlScanInterval,
87			Self::OperatorTtlScanBatchSize,
88			Self::OperatorTtlScanInterval,
89			Self::VersionEpochSampleInterval,
90			Self::HistoricalGcBatchSize,
91			Self::HistoricalGcInterval,
92			Self::CdcTtlDuration,
93			Self::CdcTtlScanInterval,
94			Self::CdcTtlScanBatchSize,
95			Self::CdcTtlScanMaxBatchesPerTick,
96			Self::CdcCompactInterval,
97			Self::CdcCompactBlockSize,
98			Self::CdcCompactSafetyLag,
99			Self::CdcCompactMaxBlocksPerTick,
100			Self::CdcCompactBlockCacheCapacity,
101			Self::CdcCompactZstdLevel,
102			Self::CdcRecentCacheCapacity,
103			Self::CdcWalAutocheckpoint,
104			Self::MultiReadBufferPages,
105			Self::MultiReadBufferPageSize,
106			Self::MultiFlushInterval,
107			Self::MultiWalAutocheckpoint,
108			Self::FlowTick,
109			Self::CdcWatermarkWaitTimeout,
110			Self::CdcConsumeWaitTimeout,
111			Self::FlowJoinProbeBlockSize,
112			Self::ThreadsAsync,
113			Self::ThreadsCoordination,
114			Self::ThreadsFlow,
115			Self::ThreadsTask,
116			Self::ThreadsCompute,
117			Self::SubscriptionWorkerThreads,
118			Self::RuntimeMetricsInterval,
119			Self::MetricFlushInterval,
120			Self::MetricsRuntimeRetention,
121			Self::MetricsProfilerRetention,
122			Self::MetricsProfilerSnapshotInterval,
123		]
124	}
125
126	pub fn default_value(&self) -> Value {
127		match self {
128			Self::OracleWindowSize => Value::Uint8(500),
129			Self::OracleWaterMark => Value::Uint8(20),
130			Self::QueryRowBatchSize => Value::Uint2(32),
131			Self::RowTtlScanBatchSize => Value::Uint8(10000),
132			Self::RowTtlScanInterval => Value::duration_seconds(60),
133			Self::OperatorTtlScanBatchSize => Value::Uint8(10000),
134			Self::OperatorTtlScanInterval => Value::duration_seconds(60),
135			Self::VersionEpochSampleInterval => Value::duration_seconds(1),
136			Self::HistoricalGcBatchSize => Value::Uint8(50_000),
137			Self::HistoricalGcInterval => Value::duration_seconds(30),
138			Self::CdcTtlDuration => Value::None {
139				inner: ValueType::Duration,
140			},
141			Self::CdcTtlScanInterval => Value::duration_seconds(30),
142			Self::CdcTtlScanBatchSize => Value::Uint8(8192),
143			Self::CdcTtlScanMaxBatchesPerTick => Value::Uint8(32),
144			Self::CdcCompactInterval => Value::duration_seconds(60),
145			Self::CdcCompactBlockSize => Value::Uint8(1024),
146			Self::CdcCompactSafetyLag => Value::Uint8(1024),
147			Self::CdcCompactMaxBlocksPerTick => Value::Uint8(16),
148			Self::CdcCompactBlockCacheCapacity => Value::Uint8(8),
149			Self::CdcCompactZstdLevel => Value::Uint1(2),
150			Self::CdcRecentCacheCapacity => Value::Uint8(128),
151			Self::CdcWalAutocheckpoint => Value::Uint8(10000),
152			Self::MultiReadBufferPages => Value::Uint8(1024),
153			Self::MultiReadBufferPageSize => Value::Uint8(65536),
154			Self::MultiFlushInterval => Value::duration_seconds(5),
155			Self::MultiWalAutocheckpoint => Value::Uint8(10000),
156			Self::FlowTick => Value::duration_seconds(1),
157			Self::CdcWatermarkWaitTimeout => Value::duration_seconds(1),
158			Self::CdcConsumeWaitTimeout => Value::duration_seconds(30),
159			Self::FlowJoinProbeBlockSize => Value::Uint8(1024),
160			Self::ThreadsAsync => Value::Uint2(1),
161			Self::ThreadsCoordination => Value::Uint2(2),
162			Self::ThreadsFlow => Value::Uint2(2),
163			Self::ThreadsTask => Value::Uint2(2),
164			Self::ThreadsCompute => Value::Uint2(2),
165			Self::SubscriptionWorkerThreads => Value::Uint2(0),
166			Self::RuntimeMetricsInterval => Value::duration_seconds(5),
167			Self::MetricFlushInterval => Value::duration_seconds(10),
168			Self::MetricsRuntimeRetention => Value::duration_seconds(7 * 24 * 3600),
169			Self::MetricsProfilerRetention => Value::duration_seconds(3600),
170			Self::MetricsProfilerSnapshotInterval => Value::None {
171				inner: ValueType::Duration,
172			},
173		}
174	}
175
176	pub fn description(&self) -> &'static str {
177		match self {
178			Self::OracleWindowSize => "Number of transactions per conflict-detection window.",
179			Self::OracleWaterMark => "Number of conflict windows retained before cleanup is triggered.",
180			Self::QueryRowBatchSize => {
181				"Number of rows produced per batch by query / DML pipeline operators."
182			}
183			Self::RowTtlScanBatchSize => "Max rows to examine per batch during a row TTL scan.",
184			Self::RowTtlScanInterval => "How often the row TTL actor should scan for expired rows.",
185			Self::OperatorTtlScanBatchSize => {
186				"Max rows to examine per batch during an operator-state TTL scan."
187			}
188			Self::OperatorTtlScanInterval => {
189				"How often the operator-state TTL actor should scan for expired rows."
190			}
191			Self::VersionEpochSampleInterval => {
192				"How often the version-epoch sampler records a (wall-clock, commit version) sample used to map a TTL duration to a cutoff version."
193			}
194			Self::HistoricalGcBatchSize => {
195				"Max historical (key, version) pairs scanned per shape per historical GC tick."
196			}
197			Self::HistoricalGcInterval => {
198				"How often the historical-version GC actor sweeps __historical for versions older than the oracle read watermark."
199			}
200			Self::CdcTtlDuration => {
201				"Maximum age of CDC entries before eviction. When unset, CDC is retained forever; \
202				 when set, must be > 0 and entries older than this duration are evicted regardless \
203				 of consumer state."
204			}
205			Self::CdcTtlScanInterval => {
206				"How often the CDC producer actor scans for and evicts expired CDC entries."
207			}
208			Self::CdcTtlScanBatchSize => {
209				"Max CDC entries deleted per transaction during a CDC TTL eviction tick."
210			}
211			Self::CdcTtlScanMaxBatchesPerTick => {
212				"Upper bound on delete transactions per CDC TTL eviction tick. Caps how long one tick can run when draining a backlog; remaining work continues on the next tick."
213			}
214			Self::CdcCompactInterval => "How often the CDC compaction actor runs.",
215			Self::CdcCompactBlockSize => "Number of CDC entries packed into one compressed block.",
216			Self::CdcCompactSafetyLag => "Versions newer than (max_version - lag) are never compacted.",
217			Self::CdcCompactMaxBlocksPerTick => {
218				"Upper bound on consecutive blocks produced per actor tick."
219			}
220			Self::CdcCompactBlockCacheCapacity => {
221				"Number of decompressed CDC blocks held in the in-memory LRU cache."
222			}
223			Self::CdcCompactZstdLevel => {
224				"Zstd compression level for CDC blocks. Range 1-22; higher means smaller blocks but \
225				 slower compression. Decompression cost is independent of level."
226			}
227			Self::CdcRecentCacheCapacity => {
228				"Number of most-recent decoded CDC entries held in memory so a caught-up consumer \
229				 is served without re-reading and re-deserializing from the backend."
230			}
231			Self::CdcWalAutocheckpoint => {
232				"WAL frame threshold (SQLite wal_autocheckpoint PRAGMA) for the CDC log's SQLite tier. \
233				 CDC has no explicit checkpoint of its own, so this is the sole control over how often \
234				 cdc.db's WAL is checkpointed into the main file. Higher values checkpoint less often with \
235				 a larger WAL; since CDC is written on the commit path, this also bounds how often a commit \
236				 pays an inline auto-checkpoint. Read once at boot; changing it requires a restart."
237			}
238			Self::MultiReadBufferPages => {
239				"Number of pages (contiguous row-number buckets) the multi-version read cache keeps \
240				 resident before eviction. Raising it trades RAM for fewer persistent-tier reads."
241			}
242			Self::MultiReadBufferPageSize => {
243				"Number of rows per cached page (bucket) in the multi-version read cache. Must be a \
244				 power of two; sets the granularity of whole-page read-ahead and completeness tracking."
245			}
246			Self::MultiFlushInterval => {
247				"How often the persistent-flush actor drains the in-memory commit buffer into the multi \
248				 store's SQLite tier and checkpoints its WAL. Longer intervals coalesce more writes per \
249				 flush - fewer, larger WAL checkpoints and a larger WAL - at the cost of more resident \
250				 commit-buffer memory and a longer window before data is materialized in the persistent \
251				 file. Read once at boot; changing it requires a restart."
252			}
253			Self::MultiWalAutocheckpoint => {
254				"WAL frame threshold for the multi store's SQLite tier: sets both the SQLite \
255				 wal_autocheckpoint PRAGMA and the frame count above which the flush actor forces a \
256				 blocking RESTART checkpoint. Higher values checkpoint less often with a larger WAL, \
257				 reducing checkpoint I/O and blocking-checkpoint frequency; lower values keep the WAL \
258				 small at the cost of more frequent checkpoints. Read once at boot; changing it requires \
259				 a restart."
260			}
261			Self::FlowTick => {
262				"How often the deferred and transactional flow tick coordinators wake up to dispatch \
263				 due flows."
264			}
265			Self::CdcWatermarkWaitTimeout => {
266				"Backstop timeout for the CDC consumer's wait for the transaction watermark to reach the \
267				 latest commit before consuming; catch-up is event-driven, so this only bounds a missed \
268				 wakeup. Must be > 0."
269			}
270			Self::CdcConsumeWaitTimeout => {
271				"Backstop timeout for the CDC consumer's wait for a consume reply from the downstream \
272				 consumer. A lost reply would otherwise wedge the poll loop forever; on timeout the batch \
273				 is re-dispatched without advancing the checkpoint. Must be > 0."
274			}
275			Self::FlowJoinProbeBlockSize => {
276				"Number of opposite-side rows a streaming join pulls per block when probing its stored \
277				 state. Bounds resident probe memory without dropping matches; smaller trades fewer \
278				 resident rows for more scan round-trips."
279			}
280			Self::ThreadsAsync => {
281				"Number of worker threads for the async runtime. Must be >= 1. \
282				 Read at boot before the runtime starts; changes require restart."
283			}
284			Self::ThreadsCoordination => {
285				"Number of worker threads for the coordination group (long-lived actors with \
286				 tiny high-frequency handlers and periodic background actors); pinned dispatch. \
287				 Must be >= 1. Changes require restart."
288			}
289			Self::ThreadsFlow => {
290				"Number of worker threads for the flow group (long-lived heavy-handler actors: \
291				 materialized-view flow execution); pinned dispatch. \
292				 Must be >= 1. Changes require restart."
293			}
294			Self::ThreadsTask => {
295				"Number of worker threads for the task pool (short-lived work: per-request \
296				 actors and one-shot jobs). Must be >= 1. Changes require restart."
297			}
298			Self::ThreadsCompute => {
299				"Number of worker threads for the compute pool (data-parallel work via install(), \
300				 never actors). Must be >= 1. Changes require restart."
301			}
302			Self::SubscriptionWorkerThreads => {
303				"Number of subscription worker actors that fan out CDC changes to ephemeral \
304				 subscriptions in parallel. 0 means auto (size to the system thread pool). Higher values \
305				 raise fan-out parallelism for many concurrent subscriptions. Changes require restart."
306			}
307			Self::RuntimeMetricsInterval => {
308				"How often the runtime-metrics sampler records a memory snapshot into \
309				 system::metrics::runtime::memory::snapshots. When unset, the history sampler is \
310				 dormant and only the live ::current view is available; when set, must be > 0."
311			}
312			Self::MetricFlushInterval => {
313				"How often the metric collector flushes accumulated storage and CDC stats into the \
314				 system::metrics views. Must be > 0."
315			}
316			Self::MetricsRuntimeRetention => {
317				"Row TTL applied to the system::metrics::runtime::* snapshot series so old samples are \
318				 evicted. Seeded onto each runtime series at bootstrap only when it has no row settings \
319				 yet; changing it affects series created after the change, not already-seeded ones. \
320				 Must be > 0."
321			}
322			Self::MetricsProfilerRetention => {
323				"Row TTL applied to the system::metrics::profiler::*::snapshots series so old samples are \
324				 evicted. Seeded onto each profiler series at bootstrap only when it has no row settings \
325				 yet; changing it affects series created after the change, not already-seeded ones. \
326				 Must be > 0."
327			}
328			Self::MetricsProfilerSnapshotInterval => {
329				"How often the profiler snapshot actor flushes in-memory aggregates into \
330				 system::metrics::profiler::*::snapshots. Defaults to none, which disables snapshot \
331				 persistence entirely (the actor is never spawned) and leaves only the live ::current \
332				 view available; when set, must be > 0. Read once at subsystem construction, so \
333				 changing it requires a restart."
334			}
335		}
336	}
337
338	pub fn requires_restart(&self) -> bool {
339		match self {
340			Self::OracleWindowSize => false,
341			Self::OracleWaterMark => false,
342			Self::QueryRowBatchSize => false,
343			Self::RowTtlScanBatchSize => false,
344			Self::RowTtlScanInterval => false,
345			Self::OperatorTtlScanBatchSize => false,
346			Self::OperatorTtlScanInterval => false,
347			Self::VersionEpochSampleInterval => false,
348			Self::HistoricalGcBatchSize => false,
349			Self::HistoricalGcInterval => false,
350			Self::CdcTtlDuration => false,
351			Self::CdcTtlScanInterval => true,
352			Self::CdcTtlScanBatchSize => false,
353			Self::CdcTtlScanMaxBatchesPerTick => false,
354			Self::CdcCompactInterval => false,
355			Self::CdcCompactBlockSize => false,
356			Self::CdcCompactSafetyLag => false,
357			Self::CdcCompactMaxBlocksPerTick => false,
358			Self::CdcCompactBlockCacheCapacity => true,
359			Self::CdcCompactZstdLevel => false,
360			Self::CdcRecentCacheCapacity => true,
361			Self::CdcWalAutocheckpoint => true,
362			Self::MultiReadBufferPages => true,
363			Self::MultiReadBufferPageSize => true,
364			Self::MultiFlushInterval => true,
365			Self::MultiWalAutocheckpoint => true,
366			Self::FlowTick => false,
367			Self::CdcWatermarkWaitTimeout => false,
368			Self::CdcConsumeWaitTimeout => false,
369			Self::FlowJoinProbeBlockSize => false,
370			Self::ThreadsAsync => true,
371			Self::ThreadsCoordination => true,
372			Self::ThreadsFlow => true,
373			Self::ThreadsTask => true,
374			Self::ThreadsCompute => true,
375			Self::SubscriptionWorkerThreads => true,
376			Self::RuntimeMetricsInterval => false,
377			Self::MetricFlushInterval => false,
378			Self::MetricsRuntimeRetention => true,
379			Self::MetricsProfilerRetention => true,
380			Self::MetricsProfilerSnapshotInterval => true,
381		}
382	}
383
384	pub fn expected_types(&self) -> &'static [ValueType] {
385		match self {
386			Self::OracleWindowSize => &[ValueType::Uint8],
387			Self::OracleWaterMark => &[ValueType::Uint8],
388			Self::QueryRowBatchSize => &[ValueType::Uint2],
389			Self::RowTtlScanBatchSize => &[ValueType::Uint8],
390			Self::RowTtlScanInterval => &[ValueType::Duration],
391			Self::OperatorTtlScanBatchSize => &[ValueType::Uint8],
392			Self::OperatorTtlScanInterval => &[ValueType::Duration],
393			Self::VersionEpochSampleInterval => &[ValueType::Duration],
394			Self::HistoricalGcBatchSize => &[ValueType::Uint8],
395			Self::HistoricalGcInterval => &[ValueType::Duration],
396			Self::CdcTtlDuration => &[ValueType::Duration],
397			Self::CdcTtlScanInterval => &[ValueType::Duration],
398			Self::CdcTtlScanBatchSize => &[ValueType::Uint8],
399			Self::CdcTtlScanMaxBatchesPerTick => &[ValueType::Uint8],
400			Self::CdcCompactInterval => &[ValueType::Duration],
401			Self::CdcCompactBlockSize => &[ValueType::Uint8],
402			Self::CdcCompactSafetyLag => &[ValueType::Uint8],
403			Self::CdcCompactMaxBlocksPerTick => &[ValueType::Uint8],
404			Self::CdcCompactBlockCacheCapacity => &[ValueType::Uint8],
405			Self::CdcCompactZstdLevel => &[ValueType::Uint1],
406			Self::CdcRecentCacheCapacity => &[ValueType::Uint8],
407			Self::CdcWalAutocheckpoint => &[ValueType::Uint8],
408			Self::MultiReadBufferPages => &[ValueType::Uint8],
409			Self::MultiReadBufferPageSize => &[ValueType::Uint8],
410			Self::MultiFlushInterval => &[ValueType::Duration],
411			Self::MultiWalAutocheckpoint => &[ValueType::Uint8],
412			Self::FlowTick => &[ValueType::Duration],
413			Self::CdcWatermarkWaitTimeout => &[ValueType::Duration],
414			Self::CdcConsumeWaitTimeout => &[ValueType::Duration],
415			Self::FlowJoinProbeBlockSize => &[ValueType::Uint8],
416			Self::ThreadsAsync => &[ValueType::Uint2],
417			Self::ThreadsCoordination => &[ValueType::Uint2],
418			Self::ThreadsFlow => &[ValueType::Uint2],
419			Self::ThreadsTask => &[ValueType::Uint2],
420			Self::ThreadsCompute => &[ValueType::Uint2],
421			Self::SubscriptionWorkerThreads => &[ValueType::Uint2],
422			Self::RuntimeMetricsInterval => &[ValueType::Duration],
423			Self::MetricFlushInterval => &[ValueType::Duration],
424			Self::MetricsRuntimeRetention => &[ValueType::Duration],
425			Self::MetricsProfilerRetention => &[ValueType::Duration],
426			Self::MetricsProfilerSnapshotInterval => &[ValueType::Duration],
427		}
428	}
429
430	pub fn is_optional(&self) -> bool {
431		match self {
432			Self::OracleWindowSize => false,
433			Self::OracleWaterMark => false,
434			Self::QueryRowBatchSize => false,
435			Self::RowTtlScanBatchSize => false,
436			Self::RowTtlScanInterval => false,
437			Self::OperatorTtlScanBatchSize => false,
438			Self::OperatorTtlScanInterval => false,
439			Self::VersionEpochSampleInterval => false,
440			Self::HistoricalGcBatchSize => false,
441			Self::HistoricalGcInterval => false,
442			Self::CdcTtlDuration => true,
443			Self::CdcTtlScanInterval => false,
444			Self::CdcTtlScanBatchSize => false,
445			Self::CdcTtlScanMaxBatchesPerTick => false,
446			Self::CdcCompactInterval => false,
447			Self::CdcCompactBlockSize => false,
448			Self::CdcCompactSafetyLag => false,
449			Self::CdcCompactMaxBlocksPerTick => false,
450			Self::CdcCompactBlockCacheCapacity => false,
451			Self::CdcCompactZstdLevel => false,
452			Self::CdcRecentCacheCapacity => false,
453			Self::CdcWalAutocheckpoint => false,
454			Self::MultiReadBufferPages => false,
455			Self::MultiReadBufferPageSize => false,
456			Self::MultiFlushInterval => false,
457			Self::MultiWalAutocheckpoint => false,
458			Self::FlowTick => false,
459			Self::CdcWatermarkWaitTimeout => false,
460			Self::CdcConsumeWaitTimeout => false,
461			Self::FlowJoinProbeBlockSize => false,
462			Self::ThreadsAsync => false,
463			Self::ThreadsCoordination => false,
464			Self::ThreadsFlow => false,
465			Self::ThreadsTask => false,
466			Self::ThreadsCompute => false,
467			Self::SubscriptionWorkerThreads => false,
468			Self::RuntimeMetricsInterval => true,
469			Self::MetricFlushInterval => false,
470			Self::MetricsRuntimeRetention => false,
471			Self::MetricsProfilerRetention => false,
472			Self::MetricsProfilerSnapshotInterval => true,
473		}
474	}
475
476	fn validate_canonical(&self, value: &Value) -> Result<(), String> {
477		match self {
478			Self::CdcTtlDuration => match value {
479				Value::None {
480					..
481				} => Ok(()),
482				Value::Duration(d) => {
483					if d.is_positive() {
484						Ok(())
485					} else {
486						Err("CDC_TTL_DURATION must be greater than zero".to_string())
487					}
488				}
489				_ => Ok(()),
490			},
491			Self::CdcCompactInterval => match value {
492				Value::Duration(d) => {
493					if d.is_positive() {
494						Ok(())
495					} else {
496						Err("CDC_COMPACT_INTERVAL must be greater than zero".to_string())
497					}
498				}
499				_ => Ok(()),
500			},
501			Self::CdcCompactBlockSize => match value {
502				Value::Uint8(0) => Err("CDC_COMPACT_BLOCK_SIZE must be greater than zero".to_string()),
503				_ => Ok(()),
504			},
505			Self::QueryRowBatchSize => match value {
506				Value::Uint2(0) => Err("QUERY_ROW_BATCH_SIZE must be greater than zero".to_string()),
507				_ => Ok(()),
508			},
509			Self::CdcCompactBlockCacheCapacity => match value {
510				Value::Uint8(0) => {
511					Err("CDC_COMPACT_BLOCK_CACHE_CAPACITY must be greater than zero".to_string())
512				}
513				_ => Ok(()),
514			},
515			Self::MultiReadBufferPages => match value {
516				Value::Uint8(0) => Err("MULTI_READ_BUFFER_PAGES must be greater than zero".to_string()),
517				_ => Ok(()),
518			},
519			Self::MultiReadBufferPageSize => match value {
520				Value::Uint8(v) if v.is_power_of_two() => Ok(()),
521				Value::Uint8(_) => {
522					Err("MULTI_READ_BUFFER_PAGE_SIZE must be a power of two".to_string())
523				}
524				_ => Ok(()),
525			},
526			Self::MultiFlushInterval => match value {
527				Value::Duration(d) if d.is_positive() => Ok(()),
528				Value::Duration(_) => Err("MULTI_FLUSH_INTERVAL must be greater than zero".to_string()),
529				_ => Ok(()),
530			},
531			Self::MultiWalAutocheckpoint => match value {
532				Value::Uint8(0) => {
533					Err("MULTI_WAL_AUTOCHECKPOINT must be greater than zero".to_string())
534				}
535				_ => Ok(()),
536			},
537			Self::CdcWalAutocheckpoint => match value {
538				Value::Uint8(0) => Err("CDC_WAL_AUTOCHECKPOINT must be greater than zero".to_string()),
539				_ => Ok(()),
540			},
541			Self::CdcCompactZstdLevel => match value {
542				Value::Uint1(v) if (1..=22).contains(v) => Ok(()),
543				Value::Uint1(_) => Err("CDC_COMPACT_ZSTD_LEVEL must be in [1, 22]".to_string()),
544				_ => Ok(()),
545			},
546			Self::HistoricalGcBatchSize => match value {
547				Value::Uint8(0) => {
548					Err("HISTORICAL_GC_BATCH_SIZE must be greater than zero".to_string())
549				}
550				_ => Ok(()),
551			},
552			Self::HistoricalGcInterval => match value {
553				Value::Duration(d) => {
554					if d.is_positive() {
555						Ok(())
556					} else {
557						Err("HISTORICAL_GC_INTERVAL must be greater than zero".to_string())
558					}
559				}
560				_ => Ok(()),
561			},
562			Self::FlowTick => match value {
563				Value::Duration(d) => {
564					if d.is_positive() {
565						Ok(())
566					} else {
567						Err("FLOW_TICK must be greater than zero".to_string())
568					}
569				}
570				_ => Ok(()),
571			},
572			Self::CdcWatermarkWaitTimeout => match value {
573				Value::Duration(d) => {
574					if d.is_positive() {
575						Ok(())
576					} else {
577						Err("CDC_WATERMARK_WAIT_TIMEOUT must be greater than zero".to_string())
578					}
579				}
580				_ => Ok(()),
581			},
582			Self::CdcConsumeWaitTimeout => match value {
583				Value::Duration(d) => {
584					if d.is_positive() {
585						Ok(())
586					} else {
587						Err("CDC_CONSUME_WAIT_TIMEOUT must be greater than zero".to_string())
588					}
589				}
590				_ => Ok(()),
591			},
592			Self::FlowJoinProbeBlockSize => match value {
593				Value::Uint8(0) => {
594					Err("FLOW_JOIN_PROBE_BLOCK_SIZE must be greater than zero".to_string())
595				}
596				_ => Ok(()),
597			},
598			Self::ThreadsAsync => match value {
599				Value::Uint2(0) => Err("THREADS_ASYNC must be greater than zero".to_string()),
600				_ => Ok(()),
601			},
602			Self::ThreadsCoordination => match value {
603				Value::Uint2(0) => Err("THREADS_COORDINATION must be greater than zero".to_string()),
604				_ => Ok(()),
605			},
606			Self::ThreadsFlow => match value {
607				Value::Uint2(0) => Err("THREADS_FLOW must be greater than zero".to_string()),
608				_ => Ok(()),
609			},
610			Self::ThreadsTask => match value {
611				Value::Uint2(0) => Err("THREADS_TASK must be greater than zero".to_string()),
612				_ => Ok(()),
613			},
614			Self::ThreadsCompute => match value {
615				Value::Uint2(0) => Err("THREADS_COMPUTE must be greater than zero".to_string()),
616				_ => Ok(()),
617			},
618			Self::SubscriptionWorkerThreads => Ok(()),
619			Self::RuntimeMetricsInterval => match value {
620				Value::None {
621					..
622				} => Ok(()),
623				Value::Duration(d) => {
624					if d.is_positive() {
625						Ok(())
626					} else {
627						Err("RUNTIME_METRICS_INTERVAL must be greater than zero".to_string())
628					}
629				}
630				_ => Ok(()),
631			},
632			Self::MetricFlushInterval => match value {
633				Value::Duration(d) => {
634					if d.is_positive() {
635						Ok(())
636					} else {
637						Err("METRIC_FLUSH_INTERVAL must be greater than zero".to_string())
638					}
639				}
640				_ => Ok(()),
641			},
642			Self::MetricsRuntimeRetention => match value {
643				Value::Duration(d) => {
644					if d.is_positive() {
645						Ok(())
646					} else {
647						Err("METRICS_RUNTIME_RETENTION must be greater than zero".to_string())
648					}
649				}
650				_ => Ok(()),
651			},
652			Self::MetricsProfilerRetention => match value {
653				Value::Duration(d) => {
654					if d.is_positive() {
655						Ok(())
656					} else {
657						Err("METRICS_PROFILER_RETENTION must be greater than zero".to_string())
658					}
659				}
660				_ => Ok(()),
661			},
662			Self::MetricsProfilerSnapshotInterval => match value {
663				Value::None {
664					..
665				} => Ok(()),
666				Value::Duration(d) => {
667					if d.is_positive() {
668						Ok(())
669					} else {
670						Err("METRICS_PROFILER_SNAPSHOT_INTERVAL must be greater than zero"
671							.to_string())
672					}
673				}
674				_ => Ok(()),
675			},
676			_ => Ok(()),
677		}
678	}
679
680	pub fn accept(&self, value: Value) -> Result<Value, AcceptError> {
681		if let Value::None {
682			inner,
683		} = &value
684		{
685			if self.is_optional() && self.expected_types().contains(inner) {
686				return Ok(value);
687			}
688			return Err(AcceptError::TypeMismatch {
689				expected: self.expected_types().to_vec(),
690				actual: value.get_type(),
691			});
692		}
693
694		if !self.expected_types().contains(&value.get_type()) {
695			return Err(AcceptError::TypeMismatch {
696				expected: self.expected_types().to_vec(),
697				actual: value.get_type(),
698			});
699		}
700
701		self.validate_canonical(&value).map_err(AcceptError::InvalidValue)?;
702		Ok(value)
703	}
704}
705
706impl fmt::Display for ConfigKey {
707	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
708		match self {
709			Self::OracleWindowSize => write!(f, "ORACLE_WINDOW_SIZE"),
710			Self::OracleWaterMark => write!(f, "ORACLE_WATER_MARK"),
711			Self::QueryRowBatchSize => write!(f, "QUERY_ROW_BATCH_SIZE"),
712			Self::RowTtlScanBatchSize => write!(f, "ROW_TTL_SCAN_BATCH_SIZE"),
713			Self::RowTtlScanInterval => write!(f, "ROW_TTL_SCAN_INTERVAL"),
714			Self::OperatorTtlScanBatchSize => write!(f, "OPERATOR_TTL_SCAN_BATCH_SIZE"),
715			Self::OperatorTtlScanInterval => write!(f, "OPERATOR_TTL_SCAN_INTERVAL"),
716			Self::VersionEpochSampleInterval => write!(f, "VERSION_EPOCH_SAMPLE_INTERVAL"),
717			Self::HistoricalGcBatchSize => write!(f, "HISTORICAL_GC_BATCH_SIZE"),
718			Self::HistoricalGcInterval => write!(f, "HISTORICAL_GC_INTERVAL"),
719			Self::CdcTtlDuration => write!(f, "CDC_TTL_DURATION"),
720			Self::CdcTtlScanInterval => write!(f, "CDC_TTL_SCAN_INTERVAL"),
721			Self::CdcTtlScanBatchSize => write!(f, "CDC_TTL_SCAN_BATCH_SIZE"),
722			Self::CdcTtlScanMaxBatchesPerTick => write!(f, "CDC_TTL_SCAN_MAX_BATCHES_PER_TICK"),
723			Self::CdcCompactInterval => write!(f, "CDC_COMPACT_INTERVAL"),
724			Self::CdcCompactBlockSize => write!(f, "CDC_COMPACT_BLOCK_SIZE"),
725			Self::CdcCompactSafetyLag => write!(f, "CDC_COMPACT_SAFETY_LAG"),
726			Self::CdcCompactMaxBlocksPerTick => write!(f, "CDC_COMPACT_MAX_BLOCKS_PER_TICK"),
727			Self::CdcCompactBlockCacheCapacity => write!(f, "CDC_COMPACT_BLOCK_CACHE_CAPACITY"),
728			Self::CdcCompactZstdLevel => write!(f, "CDC_COMPACT_ZSTD_LEVEL"),
729			Self::CdcRecentCacheCapacity => write!(f, "CDC_RECENT_CACHE_CAPACITY"),
730			Self::CdcWalAutocheckpoint => write!(f, "CDC_WAL_AUTOCHECKPOINT"),
731			Self::MultiReadBufferPages => write!(f, "MULTI_READ_BUFFER_PAGES"),
732			Self::MultiReadBufferPageSize => write!(f, "MULTI_READ_BUFFER_PAGE_SIZE"),
733			Self::MultiFlushInterval => write!(f, "MULTI_FLUSH_INTERVAL"),
734			Self::MultiWalAutocheckpoint => write!(f, "MULTI_WAL_AUTOCHECKPOINT"),
735			Self::FlowTick => write!(f, "FLOW_TICK"),
736			Self::CdcWatermarkWaitTimeout => write!(f, "CDC_WATERMARK_WAIT_TIMEOUT"),
737			Self::CdcConsumeWaitTimeout => write!(f, "CDC_CONSUME_WAIT_TIMEOUT"),
738			Self::FlowJoinProbeBlockSize => write!(f, "FLOW_JOIN_PROBE_BLOCK_SIZE"),
739			Self::ThreadsAsync => write!(f, "THREADS_ASYNC"),
740			Self::ThreadsCoordination => write!(f, "THREADS_COORDINATION"),
741			Self::ThreadsFlow => write!(f, "THREADS_FLOW"),
742			Self::ThreadsTask => write!(f, "THREADS_TASK"),
743			Self::ThreadsCompute => write!(f, "THREADS_COMPUTE"),
744			Self::SubscriptionWorkerThreads => write!(f, "SUBSCRIPTION_WORKER_THREADS"),
745			Self::RuntimeMetricsInterval => write!(f, "RUNTIME_METRICS_INTERVAL"),
746			Self::MetricFlushInterval => write!(f, "METRIC_FLUSH_INTERVAL"),
747			Self::MetricsRuntimeRetention => write!(f, "METRICS_RUNTIME_RETENTION"),
748			Self::MetricsProfilerRetention => write!(f, "METRICS_PROFILER_RETENTION"),
749			Self::MetricsProfilerSnapshotInterval => write!(f, "METRICS_PROFILER_SNAPSHOT_INTERVAL"),
750		}
751	}
752}
753
754impl FromStr for ConfigKey {
755	type Err = String;
756
757	fn from_str(s: &str) -> Result<Self, Self::Err> {
758		match s {
759			"ORACLE_WINDOW_SIZE" => Ok(Self::OracleWindowSize),
760			"ORACLE_WATER_MARK" => Ok(Self::OracleWaterMark),
761			"QUERY_ROW_BATCH_SIZE" => Ok(Self::QueryRowBatchSize),
762			"ROW_TTL_SCAN_BATCH_SIZE" => Ok(Self::RowTtlScanBatchSize),
763			"ROW_TTL_SCAN_INTERVAL" => Ok(Self::RowTtlScanInterval),
764			"OPERATOR_TTL_SCAN_BATCH_SIZE" => Ok(Self::OperatorTtlScanBatchSize),
765			"OPERATOR_TTL_SCAN_INTERVAL" => Ok(Self::OperatorTtlScanInterval),
766			"VERSION_EPOCH_SAMPLE_INTERVAL" => Ok(Self::VersionEpochSampleInterval),
767			"HISTORICAL_GC_BATCH_SIZE" => Ok(Self::HistoricalGcBatchSize),
768			"HISTORICAL_GC_INTERVAL" => Ok(Self::HistoricalGcInterval),
769			"CDC_TTL_DURATION" => Ok(Self::CdcTtlDuration),
770			"CDC_TTL_SCAN_INTERVAL" => Ok(Self::CdcTtlScanInterval),
771			"CDC_TTL_SCAN_BATCH_SIZE" => Ok(Self::CdcTtlScanBatchSize),
772			"CDC_TTL_SCAN_MAX_BATCHES_PER_TICK" => Ok(Self::CdcTtlScanMaxBatchesPerTick),
773			"CDC_COMPACT_INTERVAL" => Ok(Self::CdcCompactInterval),
774			"CDC_COMPACT_BLOCK_SIZE" => Ok(Self::CdcCompactBlockSize),
775			"CDC_COMPACT_SAFETY_LAG" => Ok(Self::CdcCompactSafetyLag),
776			"CDC_COMPACT_MAX_BLOCKS_PER_TICK" => Ok(Self::CdcCompactMaxBlocksPerTick),
777			"CDC_COMPACT_BLOCK_CACHE_CAPACITY" => Ok(Self::CdcCompactBlockCacheCapacity),
778			"CDC_COMPACT_ZSTD_LEVEL" => Ok(Self::CdcCompactZstdLevel),
779			"CDC_RECENT_CACHE_CAPACITY" => Ok(Self::CdcRecentCacheCapacity),
780			"CDC_WAL_AUTOCHECKPOINT" => Ok(Self::CdcWalAutocheckpoint),
781			"MULTI_READ_BUFFER_PAGES" => Ok(Self::MultiReadBufferPages),
782			"MULTI_READ_BUFFER_PAGE_SIZE" => Ok(Self::MultiReadBufferPageSize),
783			"MULTI_FLUSH_INTERVAL" => Ok(Self::MultiFlushInterval),
784			"MULTI_WAL_AUTOCHECKPOINT" => Ok(Self::MultiWalAutocheckpoint),
785			"FLOW_TICK" => Ok(Self::FlowTick),
786			"CDC_WATERMARK_WAIT_TIMEOUT" => Ok(Self::CdcWatermarkWaitTimeout),
787			"CDC_CONSUME_WAIT_TIMEOUT" => Ok(Self::CdcConsumeWaitTimeout),
788			"FLOW_JOIN_PROBE_BLOCK_SIZE" => Ok(Self::FlowJoinProbeBlockSize),
789			"THREADS_ASYNC" => Ok(Self::ThreadsAsync),
790			"THREADS_COORDINATION" => Ok(Self::ThreadsCoordination),
791			"THREADS_FLOW" => Ok(Self::ThreadsFlow),
792			"THREADS_TASK" => Ok(Self::ThreadsTask),
793			"THREADS_COMPUTE" => Ok(Self::ThreadsCompute),
794			"SUBSCRIPTION_WORKER_THREADS" => Ok(Self::SubscriptionWorkerThreads),
795			"RUNTIME_METRICS_INTERVAL" => Ok(Self::RuntimeMetricsInterval),
796			"METRIC_FLUSH_INTERVAL" => Ok(Self::MetricFlushInterval),
797			"METRICS_RUNTIME_RETENTION" => Ok(Self::MetricsRuntimeRetention),
798			"METRICS_PROFILER_RETENTION" => Ok(Self::MetricsProfilerRetention),
799			"METRICS_PROFILER_SNAPSHOT_INTERVAL" => Ok(Self::MetricsProfilerSnapshotInterval),
800			_ => Err(format!("Unknown system configuration key: {}", s)),
801		}
802	}
803}
804
805#[derive(Debug, Clone)]
806pub struct Config {
807	pub key: ConfigKey,
808
809	pub value: Value,
810
811	pub default_value: Value,
812
813	pub description: &'static str,
814
815	pub requires_restart: bool,
816}
817
818pub trait GetConfig: Send + Sync {
819	fn get_config(&self, key: ConfigKey) -> Value;
820
821	fn get_config_at(&self, key: ConfigKey, version: CommitVersion) -> Value;
822
823	fn get_config_uint8(&self, key: ConfigKey) -> u64 {
824		let val = self.get_config(key);
825		match val {
826			Value::Uint8(v) => v,
827			v => panic!("config key '{}' expected Uint8, got {:?}", key, v),
828		}
829	}
830
831	fn get_config_uint1(&self, key: ConfigKey) -> u8 {
832		let val = self.get_config(key);
833		match val {
834			Value::Uint1(v) => v,
835			v => panic!("config key '{}' expected Uint1, got {:?}", key, v),
836		}
837	}
838
839	fn get_config_uint2(&self, key: ConfigKey) -> u16 {
840		let val = self.get_config(key);
841		match val {
842			Value::Uint2(v) => v,
843			v => panic!("config key '{}' expected Uint2, got {:?}", key, v),
844		}
845	}
846
847	fn get_config_duration(&self, key: ConfigKey) -> Duration {
848		let val = self.get_config(key);
849		match val {
850			Value::Duration(v) => v,
851			v => panic!("config key '{}' expected Duration, got {:?}", key, v),
852		}
853	}
854
855	fn get_config_duration_opt(&self, key: ConfigKey) -> Option<Duration> {
856		match self.get_config(key) {
857			Value::None {
858				..
859			} => None,
860			Value::Duration(v) => Some(v),
861			v => panic!("config key '{}' expected Duration or None, got {:?}", key, v),
862		}
863	}
864}
865
866#[cfg(test)]
867mod tests {
868	use super::*;
869
870	#[test]
871	fn test_cdc_ttl_default_is_typed_null() {
872		// Defaulting to Value::None means "TTL not configured" - producer skips cleanup.
873		let default = ConfigKey::CdcTtlDuration.default_value();
874		assert!(matches!(
875			default,
876			Value::None {
877				inner: ValueType::Duration
878			}
879		));
880	}
881
882	#[test]
883	fn test_cdc_ttl_accept_passes_typed_null() {
884		let none = Value::None {
885			inner: ValueType::Duration,
886		};
887		let v = ConfigKey::CdcTtlDuration.accept(none.clone()).unwrap();
888		assert_eq!(v, none);
889	}
890
891	#[test]
892	fn test_cdc_ttl_accept_passes_positive_duration() {
893		let one_sec = Value::duration_seconds(1);
894		assert_eq!(ConfigKey::CdcTtlDuration.accept(one_sec.clone()).unwrap(), one_sec);
895
896		let one_hour = Value::duration_seconds(3600);
897		assert_eq!(ConfigKey::CdcTtlDuration.accept(one_hour.clone()).unwrap(), one_hour);
898	}
899
900	#[test]
901	fn test_cdc_ttl_accept_rejects_zero() {
902		let zero = Value::duration_seconds(0);
903		match ConfigKey::CdcTtlDuration.accept(zero).unwrap_err() {
904			AcceptError::InvalidValue(reason) => {
905				assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
906			}
907			other => panic!("expected InvalidValue, got {other:?}"),
908		}
909	}
910
911	#[test]
912	fn test_cdc_ttl_accept_rejects_negative() {
913		let negative = Value::duration_seconds(-5);
914		assert!(matches!(ConfigKey::CdcTtlDuration.accept(negative), Err(AcceptError::InvalidValue(_))));
915	}
916
917	#[test]
918	fn test_other_keys_accept_in_type_values() {
919		// Keys without bespoke validation should accept any in-type value.
920		assert!(ConfigKey::OracleWindowSize.accept(Value::Uint8(0)).is_ok());
921		assert!(ConfigKey::RowTtlScanInterval.accept(Value::duration_seconds(0)).is_ok());
922	}
923
924	#[test]
925	fn test_cdc_ttl_round_trips_through_display_and_from_str() {
926		let key: ConfigKey = "CDC_TTL_DURATION".parse().unwrap();
927		assert_eq!(key, ConfigKey::CdcTtlDuration);
928		assert_eq!(format!("{}", ConfigKey::CdcTtlDuration), "CDC_TTL_DURATION");
929	}
930
931	#[test]
932	fn test_cdc_ttl_in_all() {
933		assert!(ConfigKey::all().contains(&ConfigKey::CdcTtlDuration));
934	}
935
936	#[test]
937	fn test_all_contains_every_compact_key_and_has_expected_len() {
938		let all = ConfigKey::all();
939		assert_eq!(all.len(), 41);
940		assert!(all.contains(&ConfigKey::MultiFlushInterval));
941		assert!(all.contains(&ConfigKey::MultiWalAutocheckpoint));
942		assert!(all.contains(&ConfigKey::CdcWalAutocheckpoint));
943		assert!(all.contains(&ConfigKey::MetricsRuntimeRetention));
944		assert!(all.contains(&ConfigKey::MetricsProfilerRetention));
945		assert!(all.contains(&ConfigKey::MetricsProfilerSnapshotInterval));
946		assert!(all.contains(&ConfigKey::VersionEpochSampleInterval));
947		assert!(all.contains(&ConfigKey::CdcWatermarkWaitTimeout));
948		assert!(all.contains(&ConfigKey::CdcConsumeWaitTimeout));
949		assert!(all.contains(&ConfigKey::FlowJoinProbeBlockSize));
950		assert!(all.contains(&ConfigKey::CdcTtlScanInterval));
951		assert!(all.contains(&ConfigKey::CdcTtlScanBatchSize));
952		assert!(all.contains(&ConfigKey::CdcTtlScanMaxBatchesPerTick));
953		assert!(all.contains(&ConfigKey::CdcCompactInterval));
954		assert!(all.contains(&ConfigKey::CdcCompactBlockSize));
955		assert!(all.contains(&ConfigKey::CdcCompactSafetyLag));
956		assert!(all.contains(&ConfigKey::CdcCompactMaxBlocksPerTick));
957		assert!(all.contains(&ConfigKey::CdcCompactBlockCacheCapacity));
958		assert!(all.contains(&ConfigKey::CdcCompactZstdLevel));
959		assert!(all.contains(&ConfigKey::CdcRecentCacheCapacity));
960		assert!(all.contains(&ConfigKey::MultiReadBufferPages));
961		assert!(all.contains(&ConfigKey::MultiReadBufferPageSize));
962		assert!(all.contains(&ConfigKey::QueryRowBatchSize));
963		assert!(all.contains(&ConfigKey::ThreadsAsync));
964		assert!(all.contains(&ConfigKey::ThreadsCoordination));
965		assert!(all.contains(&ConfigKey::ThreadsFlow));
966		assert!(all.contains(&ConfigKey::ThreadsTask));
967		assert!(all.contains(&ConfigKey::ThreadsCompute));
968		assert!(all.contains(&ConfigKey::RuntimeMetricsInterval));
969		assert!(all.contains(&ConfigKey::MetricFlushInterval));
970		assert!(all.contains(&ConfigKey::SubscriptionWorkerThreads));
971	}
972
973	#[test]
974	fn test_runtime_metrics_interval_metadata() {
975		// Single optional Duration knob: default on (5s), none disables the history sampler.
976		assert_eq!(ConfigKey::RuntimeMetricsInterval.default_value(), Value::duration_seconds(5));
977		assert_eq!(ConfigKey::RuntimeMetricsInterval.expected_types(), &[ValueType::Duration]);
978		assert!(ConfigKey::RuntimeMetricsInterval.is_optional());
979	}
980
981	#[test]
982	fn test_runtime_metrics_interval_round_trip() {
983		assert_eq!("RUNTIME_METRICS_INTERVAL".parse::<ConfigKey>().unwrap(), ConfigKey::RuntimeMetricsInterval);
984		assert_eq!(format!("{}", ConfigKey::RuntimeMetricsInterval), "RUNTIME_METRICS_INTERVAL");
985	}
986
987	#[test]
988	fn test_runtime_metrics_interval_accepts_none_and_positive_rejects_zero() {
989		let none = Value::None {
990			inner: ValueType::Duration,
991		};
992		assert_eq!(ConfigKey::RuntimeMetricsInterval.accept(none.clone()).unwrap(), none);
993
994		let five = Value::duration_seconds(5);
995		assert_eq!(ConfigKey::RuntimeMetricsInterval.accept(five.clone()).unwrap(), five);
996
997		let zero = Value::duration_seconds(0);
998		assert!(matches!(ConfigKey::RuntimeMetricsInterval.accept(zero), Err(AcceptError::InvalidValue(_))));
999	}
1000
1001	#[test]
1002	fn test_metric_flush_interval_metadata() {
1003		// Always-on (non-optional) Duration knob defaulting to the historical 10s flush cadence.
1004		assert_eq!(ConfigKey::MetricFlushInterval.default_value(), Value::duration_seconds(10));
1005		assert_eq!(ConfigKey::MetricFlushInterval.expected_types(), &[ValueType::Duration]);
1006		assert!(!ConfigKey::MetricFlushInterval.is_optional());
1007		assert!(!ConfigKey::MetricFlushInterval.requires_restart());
1008	}
1009
1010	#[test]
1011	fn test_metric_flush_interval_round_trip() {
1012		assert_eq!("METRIC_FLUSH_INTERVAL".parse::<ConfigKey>().unwrap(), ConfigKey::MetricFlushInterval);
1013		assert_eq!(format!("{}", ConfigKey::MetricFlushInterval), "METRIC_FLUSH_INTERVAL");
1014	}
1015
1016	#[test]
1017	fn test_metric_flush_interval_accepts_positive_rejects_zero() {
1018		let ten = Value::duration_seconds(10);
1019		assert_eq!(ConfigKey::MetricFlushInterval.accept(ten.clone()).unwrap(), ten);
1020
1021		let zero = Value::duration_seconds(0);
1022		assert!(matches!(ConfigKey::MetricFlushInterval.accept(zero), Err(AcceptError::InvalidValue(_))));
1023	}
1024
1025	#[test]
1026	fn test_cdc_recent_cache_capacity_round_trip() {
1027		assert_eq!(
1028			"CDC_RECENT_CACHE_CAPACITY".parse::<ConfigKey>().unwrap(),
1029			ConfigKey::CdcRecentCacheCapacity
1030		);
1031		assert_eq!(format!("{}", ConfigKey::CdcRecentCacheCapacity), "CDC_RECENT_CACHE_CAPACITY");
1032	}
1033
1034	#[test]
1035	fn test_cdc_recent_cache_capacity_metadata() {
1036		assert_eq!(ConfigKey::CdcRecentCacheCapacity.default_value(), Value::Uint8(128));
1037		assert_eq!(ConfigKey::CdcRecentCacheCapacity.expected_types(), &[ValueType::Uint8]);
1038		assert!(ConfigKey::CdcRecentCacheCapacity.requires_restart());
1039		assert!(!ConfigKey::CdcRecentCacheCapacity.is_optional());
1040	}
1041
1042	#[test]
1043	fn test_multi_read_buffer_pages_round_trip() {
1044		assert_eq!("MULTI_READ_BUFFER_PAGES".parse::<ConfigKey>().unwrap(), ConfigKey::MultiReadBufferPages);
1045		assert_eq!(format!("{}", ConfigKey::MultiReadBufferPages), "MULTI_READ_BUFFER_PAGES");
1046	}
1047
1048	#[test]
1049	fn test_multi_read_buffer_pages_metadata_and_rejects_zero() {
1050		assert_eq!(ConfigKey::MultiReadBufferPages.default_value(), Value::Uint8(1024));
1051		assert_eq!(ConfigKey::MultiReadBufferPages.expected_types(), &[ValueType::Uint8]);
1052		assert!(ConfigKey::MultiReadBufferPages.requires_restart());
1053		assert!(!ConfigKey::MultiReadBufferPages.is_optional());
1054		match ConfigKey::MultiReadBufferPages.accept(Value::Uint8(0)).unwrap_err() {
1055			AcceptError::InvalidValue(reason) => {
1056				assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1057			}
1058			other => panic!("expected InvalidValue, got {other:?}"),
1059		}
1060	}
1061
1062	#[test]
1063	fn test_multi_read_buffer_page_size_round_trip() {
1064		assert_eq!(
1065			"MULTI_READ_BUFFER_PAGE_SIZE".parse::<ConfigKey>().unwrap(),
1066			ConfigKey::MultiReadBufferPageSize
1067		);
1068		assert_eq!(format!("{}", ConfigKey::MultiReadBufferPageSize), "MULTI_READ_BUFFER_PAGE_SIZE");
1069	}
1070
1071	#[test]
1072	fn test_multi_read_buffer_page_size_metadata_and_rejects_non_power_of_two() {
1073		// Page size must be a power of two because pages are addressed by a row-number bit shift
1074		// (bucket = row >> shift); a non-power-of-two would not map to a single shift.
1075		assert_eq!(ConfigKey::MultiReadBufferPageSize.default_value(), Value::Uint8(65536));
1076		assert_eq!(ConfigKey::MultiReadBufferPageSize.expected_types(), &[ValueType::Uint8]);
1077		assert!(ConfigKey::MultiReadBufferPageSize.requires_restart());
1078		assert!(!ConfigKey::MultiReadBufferPageSize.is_optional());
1079		assert_eq!(
1080			ConfigKey::MultiReadBufferPageSize.accept(Value::Uint8(4096)).unwrap(),
1081			Value::Uint8(4096),
1082			"a power-of-two page size is accepted"
1083		);
1084		match ConfigKey::MultiReadBufferPageSize.accept(Value::Uint8(1000)).unwrap_err() {
1085			AcceptError::InvalidValue(reason) => {
1086				assert!(reason.contains("power of two"), "unexpected reason: {reason}");
1087			}
1088			other => panic!("expected InvalidValue, got {other:?}"),
1089		}
1090	}
1091
1092	#[test]
1093	fn test_threads_keys_round_trip() {
1094		assert_eq!("THREADS_ASYNC".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsAsync);
1095		assert_eq!("THREADS_COORDINATION".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsCoordination);
1096		assert_eq!("THREADS_FLOW".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsFlow);
1097		assert_eq!("THREADS_TASK".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsTask);
1098		assert_eq!("THREADS_COMPUTE".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsCompute);
1099		assert_eq!(format!("{}", ConfigKey::ThreadsAsync), "THREADS_ASYNC");
1100		assert_eq!(format!("{}", ConfigKey::ThreadsCoordination), "THREADS_COORDINATION");
1101		assert_eq!(format!("{}", ConfigKey::ThreadsFlow), "THREADS_FLOW");
1102		assert_eq!(format!("{}", ConfigKey::ThreadsTask), "THREADS_TASK");
1103		assert_eq!(format!("{}", ConfigKey::ThreadsCompute), "THREADS_COMPUTE");
1104	}
1105
1106	#[test]
1107	fn test_threads_defaults() {
1108		assert_eq!(ConfigKey::ThreadsAsync.default_value(), Value::Uint2(1));
1109		assert_eq!(ConfigKey::ThreadsCoordination.default_value(), Value::Uint2(2));
1110		assert_eq!(ConfigKey::ThreadsFlow.default_value(), Value::Uint2(2));
1111		assert_eq!(ConfigKey::ThreadsTask.default_value(), Value::Uint2(2));
1112		assert_eq!(ConfigKey::ThreadsCompute.default_value(), Value::Uint2(2));
1113	}
1114
1115	#[test]
1116	fn test_threads_reject_zero() {
1117		for key in [
1118			ConfigKey::ThreadsAsync,
1119			ConfigKey::ThreadsCoordination,
1120			ConfigKey::ThreadsFlow,
1121			ConfigKey::ThreadsTask,
1122			ConfigKey::ThreadsCompute,
1123		] {
1124			match key.accept(Value::Uint2(0)).unwrap_err() {
1125				AcceptError::InvalidValue(reason) => {
1126					assert!(
1127						reason.contains("greater than zero"),
1128						"{key}: unexpected reason: {reason}"
1129					);
1130				}
1131				other => panic!("{key}: expected InvalidValue, got {other:?}"),
1132			}
1133		}
1134	}
1135
1136	#[test]
1137	fn test_threads_accept_positive() {
1138		assert_eq!(ConfigKey::ThreadsAsync.accept(Value::Uint2(4)).unwrap(), Value::Uint2(4));
1139		assert_eq!(ConfigKey::ThreadsCoordination.accept(Value::Uint2(8)).unwrap(), Value::Uint2(8));
1140		assert_eq!(ConfigKey::ThreadsFlow.accept(Value::Uint2(16)).unwrap(), Value::Uint2(16));
1141		assert_eq!(ConfigKey::ThreadsTask.accept(Value::Uint2(4)).unwrap(), Value::Uint2(4));
1142		assert_eq!(ConfigKey::ThreadsCompute.accept(Value::Uint2(2)).unwrap(), Value::Uint2(2));
1143	}
1144
1145	#[test]
1146	fn test_threads_reject_int4_for_uint2_key() {
1147		// accept is strict: coercion happens at the CALL boundary via cast_value.
1148		assert!(matches!(ConfigKey::ThreadsTask.accept(Value::Int4(8)), Err(AcceptError::TypeMismatch { .. })));
1149	}
1150
1151	#[test]
1152	fn test_threads_require_restart() {
1153		assert!(ConfigKey::ThreadsAsync.requires_restart());
1154		assert!(ConfigKey::ThreadsCoordination.requires_restart());
1155		assert!(ConfigKey::ThreadsFlow.requires_restart());
1156		assert!(ConfigKey::ThreadsTask.requires_restart());
1157		assert!(ConfigKey::ThreadsCompute.requires_restart());
1158	}
1159
1160	#[test]
1161	fn test_query_row_batch_size_default_is_uint2_32() {
1162		assert_eq!(ConfigKey::QueryRowBatchSize.default_value(), Value::Uint2(32));
1163	}
1164
1165	#[test]
1166	fn test_query_row_batch_size_round_trips_through_display_and_from_str() {
1167		let key: ConfigKey = "QUERY_ROW_BATCH_SIZE".parse().unwrap();
1168		assert_eq!(key, ConfigKey::QueryRowBatchSize);
1169		assert_eq!(format!("{}", ConfigKey::QueryRowBatchSize), "QUERY_ROW_BATCH_SIZE");
1170	}
1171
1172	#[test]
1173	fn test_query_row_batch_size_accept_rejects_zero() {
1174		match ConfigKey::QueryRowBatchSize.accept(Value::Uint2(0)).unwrap_err() {
1175			AcceptError::InvalidValue(reason) => {
1176				assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1177			}
1178			other => panic!("expected InvalidValue, got {other:?}"),
1179		}
1180	}
1181
1182	#[test]
1183	fn test_query_row_batch_size_accept_passes_positive() {
1184		assert_eq!(ConfigKey::QueryRowBatchSize.accept(Value::Uint2(1)).unwrap(), Value::Uint2(1));
1185		assert_eq!(ConfigKey::QueryRowBatchSize.accept(Value::Uint2(1024)).unwrap(), Value::Uint2(1024));
1186	}
1187
1188	#[test]
1189	fn test_query_row_batch_size_rejects_mismatched_type() {
1190		// accept is strict: an Int4 no longer coerces here, regardless of the value.
1191		assert!(matches!(
1192			ConfigKey::QueryRowBatchSize.accept(Value::Int4(64)),
1193			Err(AcceptError::TypeMismatch { .. })
1194		));
1195		assert!(matches!(
1196			ConfigKey::QueryRowBatchSize.accept(Value::Int4(0)),
1197			Err(AcceptError::TypeMismatch { .. })
1198		));
1199	}
1200
1201	#[test]
1202	fn test_cdc_compact_interval_round_trips_through_display_and_from_str() {
1203		let key: ConfigKey = "CDC_COMPACT_INTERVAL".parse().unwrap();
1204		assert_eq!(key, ConfigKey::CdcCompactInterval);
1205		assert_eq!(format!("{}", ConfigKey::CdcCompactInterval), "CDC_COMPACT_INTERVAL");
1206	}
1207
1208	#[test]
1209	fn test_cdc_compact_block_size_round_trips_through_display_and_from_str() {
1210		let key: ConfigKey = "CDC_COMPACT_BLOCK_SIZE".parse().unwrap();
1211		assert_eq!(key, ConfigKey::CdcCompactBlockSize);
1212		assert_eq!(format!("{}", ConfigKey::CdcCompactBlockSize), "CDC_COMPACT_BLOCK_SIZE");
1213	}
1214
1215	#[test]
1216	fn test_cdc_compact_safety_lag_round_trips_through_display_and_from_str() {
1217		let key: ConfigKey = "CDC_COMPACT_SAFETY_LAG".parse().unwrap();
1218		assert_eq!(key, ConfigKey::CdcCompactSafetyLag);
1219		assert_eq!(format!("{}", ConfigKey::CdcCompactSafetyLag), "CDC_COMPACT_SAFETY_LAG");
1220	}
1221
1222	#[test]
1223	fn test_cdc_compact_max_blocks_per_tick_round_trips_through_display_and_from_str() {
1224		let key: ConfigKey = "CDC_COMPACT_MAX_BLOCKS_PER_TICK".parse().unwrap();
1225		assert_eq!(key, ConfigKey::CdcCompactMaxBlocksPerTick);
1226		assert_eq!(format!("{}", ConfigKey::CdcCompactMaxBlocksPerTick), "CDC_COMPACT_MAX_BLOCKS_PER_TICK");
1227	}
1228
1229	#[test]
1230	fn test_cdc_compact_interval_default_is_duration() {
1231		assert!(matches!(ConfigKey::CdcCompactInterval.default_value(), Value::Duration(_)));
1232	}
1233
1234	#[test]
1235	fn test_cdc_compact_block_size_default_is_uint8_1024() {
1236		assert_eq!(ConfigKey::CdcCompactBlockSize.default_value(), Value::Uint8(1024));
1237	}
1238
1239	#[test]
1240	fn test_cdc_compact_safety_lag_default_is_uint8_1024() {
1241		assert_eq!(ConfigKey::CdcCompactSafetyLag.default_value(), Value::Uint8(1024));
1242	}
1243
1244	#[test]
1245	fn test_cdc_compact_max_blocks_per_tick_default_is_uint8_16() {
1246		assert_eq!(ConfigKey::CdcCompactMaxBlocksPerTick.default_value(), Value::Uint8(16));
1247	}
1248
1249	#[test]
1250	fn test_cdc_compact_interval_accept_passes_positive_duration() {
1251		let one_sec = Value::duration_seconds(1);
1252		assert_eq!(ConfigKey::CdcCompactInterval.accept(one_sec.clone()).unwrap(), one_sec);
1253	}
1254
1255	#[test]
1256	fn test_cdc_compact_interval_accept_rejects_zero() {
1257		let zero = Value::duration_seconds(0);
1258		match ConfigKey::CdcCompactInterval.accept(zero).unwrap_err() {
1259			AcceptError::InvalidValue(reason) => {
1260				assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1261			}
1262			other => panic!("expected InvalidValue, got {other:?}"),
1263		}
1264	}
1265
1266	#[test]
1267	fn test_cdc_compact_interval_accept_rejects_negative() {
1268		let negative = Value::duration_seconds(-5);
1269		assert!(matches!(ConfigKey::CdcCompactInterval.accept(negative), Err(AcceptError::InvalidValue(_))));
1270	}
1271
1272	#[test]
1273	fn test_cdc_compact_block_size_accept_rejects_zero() {
1274		match ConfigKey::CdcCompactBlockSize.accept(Value::Uint8(0)).unwrap_err() {
1275			AcceptError::InvalidValue(reason) => {
1276				assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1277			}
1278			other => panic!("expected InvalidValue, got {other:?}"),
1279		}
1280	}
1281
1282	#[test]
1283	fn test_cdc_compact_block_size_accept_passes_positive() {
1284		assert_eq!(ConfigKey::CdcCompactBlockSize.accept(Value::Uint8(1)).unwrap(), Value::Uint8(1));
1285		assert_eq!(ConfigKey::CdcCompactBlockSize.accept(Value::Uint8(1024)).unwrap(), Value::Uint8(1024));
1286	}
1287
1288	#[test]
1289	fn test_cdc_compact_safety_lag_and_max_blocks_accept_zero() {
1290		assert_eq!(ConfigKey::CdcCompactSafetyLag.accept(Value::Uint8(0)).unwrap(), Value::Uint8(0));
1291		assert_eq!(ConfigKey::CdcCompactMaxBlocksPerTick.accept(Value::Uint8(0)).unwrap(), Value::Uint8(0));
1292	}
1293
1294	#[test]
1295	fn test_accept_rejects_int4_for_uint8_block_size() {
1296		// accept is strict: SET CONFIG casts to Uint8 via cast_value before calling accept.
1297		assert!(matches!(
1298			ConfigKey::CdcCompactBlockSize.accept(Value::Int4(1024)),
1299			Err(AcceptError::TypeMismatch { .. })
1300		));
1301		assert!(matches!(
1302			ConfigKey::CdcCompactBlockSize.accept(Value::Int8(2048)),
1303			Err(AcceptError::TypeMismatch { .. })
1304		));
1305	}
1306
1307	#[test]
1308	fn test_accept_rejects_zero_of_canonical_type() {
1309		match ConfigKey::CdcCompactBlockSize.accept(Value::Uint8(0)).unwrap_err() {
1310			AcceptError::InvalidValue(reason) => {
1311				assert!(reason.contains("greater than zero"));
1312			}
1313			other => panic!("expected InvalidValue, got {other:?}"),
1314		}
1315	}
1316
1317	#[test]
1318	fn test_accept_rejects_negative_int_for_uint8_key() {
1319		// to_usize() returns None for negatives -> all coercion arms fail -> TypeMismatch.
1320		assert!(matches!(
1321			ConfigKey::CdcCompactBlockSize.accept(Value::Int4(-1)),
1322			Err(AcceptError::TypeMismatch { .. })
1323		));
1324	}
1325
1326	#[test]
1327	fn test_accept_rejects_int_for_duration_key() {
1328		// Bare integers carry no unit: duration keys take Duration values (or duration
1329		// strings cast at the CALL boundary), never int-as-seconds.
1330		assert!(matches!(
1331			ConfigKey::CdcCompactInterval.accept(Value::Int4(60)),
1332			Err(AcceptError::TypeMismatch { .. })
1333		));
1334	}
1335
1336	#[test]
1337	fn test_accept_idempotent_on_canonical_uint8() {
1338		let canonical = Value::Uint8(42);
1339		assert_eq!(ConfigKey::OracleWindowSize.accept(canonical.clone()).unwrap(), canonical);
1340	}
1341
1342	#[test]
1343	fn test_accept_idempotent_on_canonical_duration() {
1344		let canonical = Value::duration_seconds(5);
1345		assert_eq!(ConfigKey::CdcCompactInterval.accept(canonical.clone()).unwrap(), canonical);
1346	}
1347
1348	#[test]
1349	fn test_accept_rejects_typed_null_for_non_optional_key() {
1350		let err = ConfigKey::CdcCompactBlockSize
1351			.accept(Value::None {
1352				inner: ValueType::Uint8,
1353			})
1354			.unwrap_err();
1355		assert!(matches!(err, AcceptError::TypeMismatch { .. }));
1356	}
1357
1358	#[test]
1359	fn test_accept_passes_typed_null_for_optional_key() {
1360		let none = Value::None {
1361			inner: ValueType::Duration,
1362		};
1363		assert_eq!(ConfigKey::CdcTtlDuration.accept(none.clone()).unwrap(), none);
1364	}
1365
1366	#[test]
1367	fn test_accept_rejects_wrong_inner_type_typed_null_for_optional_key() {
1368		// Optional key still rejects typed-null whose inner doesn't match expected_types.
1369		let err = ConfigKey::CdcTtlDuration
1370			.accept(Value::None {
1371				inner: ValueType::Uint8,
1372			})
1373			.unwrap_err();
1374		assert!(matches!(err, AcceptError::TypeMismatch { .. }));
1375	}
1376
1377	#[test]
1378	fn test_metrics_retention_round_trip() {
1379		assert_eq!(
1380			"METRICS_RUNTIME_RETENTION".parse::<ConfigKey>().unwrap(),
1381			ConfigKey::MetricsRuntimeRetention
1382		);
1383		assert_eq!(
1384			"METRICS_PROFILER_RETENTION".parse::<ConfigKey>().unwrap(),
1385			ConfigKey::MetricsProfilerRetention
1386		);
1387		assert_eq!(format!("{}", ConfigKey::MetricsRuntimeRetention), "METRICS_RUNTIME_RETENTION");
1388		assert_eq!(format!("{}", ConfigKey::MetricsProfilerRetention), "METRICS_PROFILER_RETENTION");
1389	}
1390
1391	#[test]
1392	fn test_metrics_retention_defaults_are_7d_and_1h() {
1393		// Runtime snapshots are sampled every few seconds, so a week is the cap before eviction;
1394		// profiler aggregates are far noisier, so they default to a single hour.
1395		assert_eq!(ConfigKey::MetricsRuntimeRetention.default_value(), Value::duration_seconds(7 * 24 * 3600));
1396		assert_eq!(ConfigKey::MetricsProfilerRetention.default_value(), Value::duration_seconds(3600));
1397	}
1398
1399	#[test]
1400	fn test_metrics_retention_metadata() {
1401		for key in [ConfigKey::MetricsRuntimeRetention, ConfigKey::MetricsProfilerRetention] {
1402			assert_eq!(key.expected_types(), &[ValueType::Duration], "{key}");
1403			assert!(!key.is_optional(), "{key} is always defaulted, never unset");
1404		}
1405	}
1406
1407	#[test]
1408	fn test_metrics_retention_rejects_zero() {
1409		// Zero retention would map the eviction cutoff to "now" and wipe every snapshot on the next
1410		// scan, so it must be rejected like the other positive-duration knobs.
1411		for key in [ConfigKey::MetricsRuntimeRetention, ConfigKey::MetricsProfilerRetention] {
1412			match key.accept(Value::duration_seconds(0)).unwrap_err() {
1413				AcceptError::InvalidValue(reason) => {
1414					assert!(
1415						reason.contains("greater than zero"),
1416						"{key}: unexpected reason: {reason}"
1417					);
1418				}
1419				other => panic!("{key}: expected InvalidValue, got {other:?}"),
1420			}
1421		}
1422	}
1423
1424	#[test]
1425	fn test_metrics_profiler_snapshot_interval_default_is_none() {
1426		// Snapshot persistence is opt-in: leaving this key untouched must not spawn the
1427		// ProfilerSnapshotActor or grow system::metrics::profiler::*::snapshots. A consumer
1428		// that wants persisted profiler snapshots sets this explicitly to a positive duration.
1429		assert_eq!(
1430			ConfigKey::MetricsProfilerSnapshotInterval.default_value(),
1431			Value::None {
1432				inner: ValueType::Duration,
1433			}
1434		);
1435	}
1436
1437	#[test]
1438	fn test_metrics_profiler_snapshot_interval_accepts_none_to_disable_persistence() {
1439		// None is the mechanism a consumer (e.g. raptor, which only ever reads the live
1440		// in-memory accumulator and never queries the persisted ::snapshots series) uses to
1441		// stop ProfilerSnapshotActor from being spawned at all, eliminating unbounded
1442		// system::metrics::profiler::*::snapshots disk growth.
1443		let none = Value::None {
1444			inner: ValueType::Duration,
1445		};
1446		assert_eq!(ConfigKey::MetricsProfilerSnapshotInterval.accept(none.clone()).unwrap(), none);
1447	}
1448
1449	#[test]
1450	fn test_metrics_profiler_snapshot_interval_rejects_zero_and_negative() {
1451		// A zero or negative tick interval would either busy-loop the snapshot actor or fail
1452		// to schedule its timer, so it must be rejected like every other positive-duration
1453		// knob rather than silently misbehaving at runtime.
1454		match ConfigKey::MetricsProfilerSnapshotInterval.accept(Value::duration_seconds(0)).unwrap_err() {
1455			AcceptError::InvalidValue(reason) => {
1456				assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1457			}
1458			other => panic!("expected InvalidValue, got {other:?}"),
1459		}
1460		assert!(matches!(
1461			ConfigKey::MetricsProfilerSnapshotInterval.accept(Value::duration_seconds(-5)),
1462			Err(AcceptError::InvalidValue(_))
1463		));
1464	}
1465
1466	#[test]
1467	fn test_metrics_profiler_snapshot_interval_requires_restart() {
1468		// ProfilerSnapshotActor::init() arms a single fixed-period ctx.schedule_tick(...) at
1469		// actor start and never re-reads this config live, so a change without a restart would
1470		// silently have no effect. This test exists so a future change that makes the actor
1471		// live-reconfigurable doesn't forget to flip this bit back to false.
1472		assert!(ConfigKey::MetricsProfilerSnapshotInterval.requires_restart());
1473	}
1474
1475	#[test]
1476	fn test_metrics_profiler_snapshot_interval_round_trips_through_display_and_from_str() {
1477		assert_eq!(
1478			"METRICS_PROFILER_SNAPSHOT_INTERVAL".parse::<ConfigKey>().unwrap(),
1479			ConfigKey::MetricsProfilerSnapshotInterval
1480		);
1481		assert_eq!(
1482			format!("{}", ConfigKey::MetricsProfilerSnapshotInterval),
1483			"METRICS_PROFILER_SNAPSHOT_INTERVAL"
1484		);
1485	}
1486
1487	#[test]
1488	fn test_metrics_profiler_snapshot_interval_in_all() {
1489		assert!(ConfigKey::all().contains(&ConfigKey::MetricsProfilerSnapshotInterval));
1490	}
1491
1492	#[test]
1493	fn test_historical_gc_keys_round_trip() {
1494		assert_eq!("HISTORICAL_GC_BATCH_SIZE".parse::<ConfigKey>().unwrap(), ConfigKey::HistoricalGcBatchSize);
1495		assert_eq!("HISTORICAL_GC_INTERVAL".parse::<ConfigKey>().unwrap(), ConfigKey::HistoricalGcInterval);
1496		assert_eq!(format!("{}", ConfigKey::HistoricalGcBatchSize), "HISTORICAL_GC_BATCH_SIZE");
1497		assert_eq!(format!("{}", ConfigKey::HistoricalGcInterval), "HISTORICAL_GC_INTERVAL");
1498	}
1499
1500	#[test]
1501	fn test_historical_gc_defaults() {
1502		assert_eq!(ConfigKey::HistoricalGcBatchSize.default_value(), Value::Uint8(50_000));
1503		assert!(matches!(ConfigKey::HistoricalGcInterval.default_value(), Value::Duration(_)));
1504	}
1505
1506	#[test]
1507	fn test_historical_gc_batch_size_rejects_zero() {
1508		match ConfigKey::HistoricalGcBatchSize.accept(Value::Uint8(0)).unwrap_err() {
1509			AcceptError::InvalidValue(reason) => {
1510				assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1511			}
1512			other => panic!("expected InvalidValue, got {other:?}"),
1513		}
1514	}
1515
1516	#[test]
1517	fn test_historical_gc_interval_rejects_zero() {
1518		let zero = Value::duration_seconds(0);
1519		match ConfigKey::HistoricalGcInterval.accept(zero).unwrap_err() {
1520			AcceptError::InvalidValue(reason) => {
1521				assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1522			}
1523			other => panic!("expected InvalidValue, got {other:?}"),
1524		}
1525	}
1526}