Skip to main content

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}