Skip to main content

reifydb_routine/procedure/set/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4use std::str::FromStr;
5
6use reifydb_catalog::error::CatalogError;
7use reifydb_core::{interface::catalog::config::ConfigKey, value::column::columns::Columns};
8use reifydb_transaction::transaction::Transaction;
9use reifydb_type::{
10	error::Error as TypeError,
11	fragment::Fragment,
12	params::Params,
13	value::{Value, duration::Duration, r#type::Type},
14};
15
16use crate::procedure::{Procedure, context::ProcedureContext, error::ProcedureError};
17
18/// Native procedure that sets a configuration value.
19///
20/// Accepts 2 positional arguments: key (Utf8) and value (any).
21pub struct SetConfigProcedure;
22
23impl Default for SetConfigProcedure {
24	fn default() -> Self {
25		Self::new()
26	}
27}
28
29impl SetConfigProcedure {
30	pub fn new() -> Self {
31		Self
32	}
33}
34
35impl Procedure for SetConfigProcedure {
36	fn call(&self, ctx: &ProcedureContext, tx: &mut Transaction<'_>) -> Result<Columns, ProcedureError> {
37		let (key, value) = match ctx.params {
38			Params::Positional(args) if args.len() == 2 => (args[0].clone(), args[1].clone()),
39			Params::Positional(args) => {
40				return Err(ProcedureError::ArityMismatch {
41					procedure: Fragment::internal("system::config::set"),
42					expected: 2,
43					actual: args.len(),
44				});
45			}
46			_ => {
47				return Err(ProcedureError::ArityMismatch {
48					procedure: Fragment::internal("system::config::set"),
49					expected: 2,
50					actual: 0,
51				});
52			}
53		};
54
55		let key_str = match &key {
56			Value::Utf8(s) => s.as_str().to_string(),
57			_ => {
58				return Err(ProcedureError::InvalidArgumentType {
59					procedure: Fragment::internal("system::config::set"),
60					argument_index: 0,
61					expected: vec![Type::Utf8],
62					actual: key.get_type(),
63				});
64			}
65		};
66
67		if matches!(value, Value::None { .. }) {
68			return Err(CatalogError::ConfigValueInvalid(key_str).into());
69		}
70
71		let config_key = match ConfigKey::from_str(&key_str) {
72			Ok(k) => k,
73			Err(_) => {
74				return Err(CatalogError::ConfigStorageKeyNotFound(key_str).into());
75			}
76		};
77
78		let coerced_value = coerce_config_value(config_key, value)
79			.map_err(|e| ProcedureError::Wrapped(Box::new(TypeError::from(*e))))?;
80
81		let value_clone = coerced_value.clone();
82
83		match tx {
84			Transaction::Admin(admin) => ctx.catalog.set_config(admin, config_key, coerced_value)?,
85			Transaction::Test(t) => ctx.catalog.set_config(t.inner, config_key, coerced_value)?,
86			_ => {
87				return Err(ProcedureError::ExecutionFailed {
88					procedure: Fragment::internal("system::config::set"),
89					reason: "must run in an admin transaction".to_string(),
90				});
91			}
92		}
93
94		Ok(Columns::single_row([("key", Value::Utf8(key_str)), ("value", value_clone)]))
95	}
96}
97
98fn coerce_config_value(key: ConfigKey, value: Value) -> Result<Value, Box<CatalogError>> {
99	let expected_types = key.expected_types();
100	if expected_types.contains(&value.get_type()) {
101		return Ok(value);
102	}
103
104	// Try basic coercion
105	for expected in expected_types {
106		match expected {
107			Type::Uint8 => {
108				if let Some(v) = value.to_usize()
109					&& v <= u64::MAX as usize
110				{
111					return Ok(Value::Uint8(v as u64));
112				}
113			}
114			Type::Uint4 => {
115				if let Some(v) = value.to_usize()
116					&& v <= u32::MAX as usize
117				{
118					return Ok(Value::Uint4(v as u32));
119				}
120			}
121			Type::Uint2 => {
122				if let Some(v) = value.to_usize()
123					&& v <= u16::MAX as usize
124				{
125					return Ok(Value::Uint2(v as u16));
126				}
127			}
128			Type::Uint1 => {
129				if let Some(v) = value.to_usize()
130					&& v <= u8::MAX as usize
131				{
132					return Ok(Value::Uint1(v as u8));
133				}
134			}
135			Type::Int8 => {
136				if let Some(v) = value.to_usize()
137					&& v <= i64::MAX as usize
138				{
139					return Ok(Value::Int8(v as i64));
140				}
141			}
142			Type::Int4 => {
143				if let Some(v) = value.to_usize()
144					&& v <= i32::MAX as usize
145				{
146					return Ok(Value::Int4(v as i32));
147				}
148			}
149			Type::Int2 => {
150				if let Some(v) = value.to_usize()
151					&& v <= i16::MAX as usize
152				{
153					return Ok(Value::Int2(v as i16));
154				}
155			}
156			Type::Int1 => {
157				if let Some(v) = value.to_usize()
158					&& v <= i8::MAX as usize
159				{
160					return Ok(Value::Int1(v as i8));
161				}
162			}
163			Type::Duration => {
164				if let Value::Duration(v) = value {
165					return Ok(Value::Duration(v));
166				}
167				if let Some(v) = value.to_usize()
168					&& let Ok(d) = Duration::from_seconds(v as i64)
169				{
170					return Ok(Value::Duration(d));
171				}
172			}
173			_ => {}
174		}
175	}
176
177	Err(Box::new(CatalogError::ConfigTypeMismatch {
178		key: key.to_string(),
179		expected: expected_types.to_vec(),
180		actual: value.get_type(),
181	}))
182}