mz_adapter/config/params.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::collections::BTreeSet;
11
12use mz_sql::session::vars::{ENABLE_LAUNCHDARKLY, SystemVars, Value, Var, VarInput};
13
14/// A struct that defines the system parameters that should be synchronized
15pub struct SynchronizedParameters {
16 /// The backing `SystemVars` instance. Synchronized parameters are exactly
17 /// those that are returned by [SystemVars::iter_synced].
18 system_vars: SystemVars,
19 /// A set of names identifying the synchronized variables from the above
20 /// `system_vars`.
21 ///
22 /// Derived from the above at construction time with the assumption that this
23 /// set cannot change during the lifecycle of the [SystemVars] instance.
24 synchronized: BTreeSet<&'static str>,
25 /// A set of names that identifies the synchronized parameters that have been
26 /// modified by the frontend and need to be pushed to backend.
27 modified: BTreeSet<&'static str>,
28}
29
30impl Default for SynchronizedParameters {
31 fn default() -> Self {
32 Self::new(SystemVars::default())
33 }
34}
35
36impl SynchronizedParameters {
37 pub fn new(system_vars: SystemVars) -> Self {
38 let synchronized = system_vars
39 .iter_synced()
40 .map(|v| v.name())
41 .collect::<BTreeSet<_>>();
42 Self {
43 system_vars,
44 synchronized,
45 modified: BTreeSet::new(),
46 }
47 }
48
49 pub fn is_synchronized(&self, name: &str) -> bool {
50 self.synchronized.contains(name)
51 }
52
53 /// Return a clone of the set of names of synchronized values.
54 ///
55 /// Mostly useful when we need to iterate over each value, while still
56 /// maintaining a mutable reference of the surrounding
57 /// [SynchronizedParameters] instance.
58 pub fn synchronized(&self) -> BTreeSet<&'static str> {
59 self.synchronized.clone()
60 }
61
62 /// Return a vector of [ModifiedParameter] instances that need to be pushed
63 /// to the backend and reset this set to the empty set for future calls.
64 ///
65 /// The set will start growing again as soon as we modify a parameter from
66 /// the `synchronized` set with a [SynchronizedParameters::modify] call.
67 pub fn modified(&mut self) -> Vec<ModifiedParameter> {
68 let mut modified = BTreeSet::new();
69 std::mem::swap(&mut self.modified, &mut modified);
70 self.system_vars
71 .iter_synced()
72 .filter(move |var| modified.contains(var.name()))
73 .map(|var| {
74 let name = var.name().to_string();
75 let value = var.value();
76 let is_default = self.system_vars.is_default(&name, VarInput::Flat(&value)).expect("This will never panic because both the name and the value come from a `Var` instance");
77 ModifiedParameter {
78 name,
79 value,
80 is_default,
81 }
82 })
83 .collect()
84 }
85
86 /// Get the current in-memory value for the parameter identified by the
87 /// given `name`.
88 ///
89 /// # Panics
90 ///
91 /// The method will panic if the name does not refer to a valid parameter.
92 pub fn get(&self, name: &str) -> String {
93 self.system_vars
94 .get(name)
95 .expect("valid system parameter name")
96 .value()
97 }
98
99 /// Canonicalize a raw `value` for the parameter `name` to the same formatted
100 /// form [`SynchronizedParameters::get`] returns, by parsing it through the
101 /// system var and re-formatting.
102 ///
103 /// This lets values that are equal but differently encoded compare equal.
104 /// For example LaunchDarkly serves a boolean as `"true"`, while the canonical
105 /// formatting of a `bool` system var is `"on"`. Returns `None` if `name` is
106 /// not a valid parameter or `value` does not parse for it.
107 pub fn canonicalize(&self, name: &str, value: &str) -> Option<String> {
108 self.system_vars
109 .parse(name, VarInput::Flat(value))
110 .ok()
111 .map(|value| value.format())
112 }
113
114 /// Try to modify the in-memory entry for `name` in the SystemVars backing
115 /// this [SynchronizedParameters] instance.
116 ///
117 /// This will call `SystemVars::reset` iff `value` is the default for this
118 /// `name` and `SystemVars::set` otherwise.
119 ///
120 /// As a side effect, the modified set will be changed to contain `name` iff
121 /// the in-memory entry for `name` was modified **and** `name` is in the
122 /// `synchronized` set.
123 ///
124 /// Return `true` iff the backing in-memory value for this `name` has
125 /// changed.
126 pub fn modify(&mut self, name: &str, value: &str) -> bool {
127 // It's OK to call `unwrap_or(false)` here because for fixed `name`
128 // and `value` an error in `self.is_default(name, value)` implies
129 // the same error in `self.system_vars.set(name, value)`.
130 let value = VarInput::Flat(value);
131 let modified = if self.system_vars.is_default(name, value).unwrap_or(false) {
132 self.system_vars.reset(name)
133 } else {
134 self.system_vars.set(name, value)
135 };
136
137 match modified {
138 Ok(true) => {
139 // Track modified parameters from the "synchronized" set.
140 if let Some(name) = self.synchronized.get(name) {
141 self.modified.insert(name);
142 }
143 true
144 }
145 Ok(false) => {
146 // The value was the same as the current one.
147 false
148 }
149 Err(e) => {
150 tracing::error!("cannot modify system parameter {}: {}", name, e);
151 false
152 }
153 }
154 }
155
156 pub fn enable_launchdarkly(&self) -> bool {
157 let var_name = self.get(ENABLE_LAUNCHDARKLY.name());
158 let var_input = VarInput::Flat(&var_name);
159 bool::parse(var_input).expect("This is known to be a bool")
160 }
161}
162
163pub struct ModifiedParameter {
164 pub name: String,
165 pub value: String,
166 pub is_default: bool,
167}
168
169#[cfg(test)]
170mod tests {
171 use mz_sql::session::vars::SystemVars;
172
173 use super::SynchronizedParameters;
174
175 #[mz_ore::test]
176 #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `decNumberFromInt32` on OS `linux`
177 fn test_github_18189() {
178 let vars = SystemVars::default();
179 let mut sync = SynchronizedParameters::new(vars);
180 assert!(sync.modify("allowed_cluster_replica_sizes", "1,2"));
181 assert_eq!(sync.get("allowed_cluster_replica_sizes"), r#""1", "2""#);
182 assert!(sync.modify("allowed_cluster_replica_sizes", ""));
183 assert_eq!(sync.get("allowed_cluster_replica_sizes"), "");
184 }
185
186 #[mz_ore::test]
187 #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `decNumberFromInt32` on OS `linux`
188 fn test_canonicalize_bridges_bool_encodings() {
189 let vars = SystemVars::default();
190 let sync = SynchronizedParameters::new(vars);
191
192 // A `bool` system var formats canonically as "on"/"off", while
193 // LaunchDarkly serves booleans as "true"/"false". The scoped
194 // differs-from-env test compares a raw LD value against `get()`, so
195 // canonicalization must bridge the two spellings, otherwise every
196 // boolean flag would register as differing, even on a FALLTHROUGH that
197 // serves the env-wide value. (`enable_eager_delta_joins` is a scoped
198 // `bool` feature flag, default off.)
199 let name = "enable_eager_delta_joins";
200 let off = sync.get(name);
201 assert_eq!(off, "off");
202
203 // The LD spellings canonicalize to the same form as the var's own.
204 assert_eq!(sync.canonicalize(name, "false").as_deref(), Some("off"));
205 assert_eq!(sync.canonicalize(name, "true").as_deref(), Some("on"));
206 assert_eq!(
207 sync.canonicalize(name, "off"),
208 sync.canonicalize(name, "false")
209 );
210 assert_eq!(
211 sync.canonicalize(name, "on"),
212 sync.canonicalize(name, "true")
213 );
214
215 // The crux: a scoped "false" must match the env-wide "off" baseline, so
216 // it is dropped rather than recorded as a spurious override.
217 assert_eq!(
218 sync.canonicalize(name, "false").as_deref(),
219 Some(off.as_str())
220 );
221
222 // An unparseable value yields `None`. The scoped recording path relies
223 // on this to *skip* such values rather than store them: a stored
224 // unparseable bool would later panic the optimizer's decode on every
225 // plan. See `SystemParameterFrontend::evaluate_scoped_overrides`.
226 assert_eq!(sync.canonicalize(name, "not-a-bool"), None);
227 }
228
229 #[mz_ore::test]
230 #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `decNumberFromInt32` on OS `linux`
231 fn test_vars_are_synced() {
232 let vars = SystemVars::default();
233 let sync = SynchronizedParameters::new(vars);
234
235 assert!(!sync.synchronized().is_empty());
236 }
237}