Skip to main content

mz_sql/session/vars/
constraints.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
10//! Defines constraints that can be imposed on variables.
11
12use std::fmt::Debug;
13use std::ops::{Bound, RangeBounds, RangeFrom, RangeInclusive};
14use std::time::Duration;
15
16use mz_repr::adt::numeric::Numeric;
17use mz_repr::bytes::ByteSize;
18
19use super::{Value, Var, VarError};
20
21pub static NUMERIC_NON_NEGATIVE: NumericNonNegNonNan = NumericNonNegNonNan;
22
23pub static NON_ZERO_DURATION: NonZeroDuration = NonZeroDuration;
24
25pub static NUMERIC_BOUNDED_0_1_INCLUSIVE: NumericInRange<RangeInclusive<f64>> =
26    NumericInRange(0.0f64..=1.0);
27
28pub static BYTESIZE_AT_LEAST_1MB: ByteSizeInRange<RangeFrom<ByteSize>> =
29    ByteSizeInRange(ByteSize::mb(1)..);
30
31pub static U32_AT_LEAST_1: U32InRange<RangeFrom<u32>> = U32InRange(1..);
32
33#[derive(Debug)]
34pub enum ValueConstraint {
35    /// Variable is read-only and cannot be updated.
36    ReadOnly,
37    /// The variables value can be updated, but only to a fixed value.
38    Fixed,
39    // Arbitrary constraints over values.
40    Domain(&'static dyn DynDomainConstraint),
41}
42
43impl ValueConstraint {
44    pub fn check_constraint(
45        &self,
46        var: &dyn Var,
47        cur_value: &dyn Value,
48        new_value: &dyn Value,
49    ) -> Result<(), VarError> {
50        match self {
51            ValueConstraint::ReadOnly => return Err(VarError::ReadOnlyParameter(var.name())),
52            ValueConstraint::Fixed => {
53                if cur_value != new_value {
54                    return Err(VarError::FixedValueParameter {
55                        name: var.name(),
56                        value: cur_value.format(),
57                    });
58                }
59            }
60            ValueConstraint::Domain(check) => check.check(var, new_value)?,
61        }
62
63        Ok(())
64    }
65}
66
67impl Clone for ValueConstraint {
68    fn clone(&self) -> Self {
69        match self {
70            ValueConstraint::Fixed => ValueConstraint::Fixed,
71            ValueConstraint::ReadOnly => ValueConstraint::ReadOnly,
72            ValueConstraint::Domain(c) => ValueConstraint::Domain(*c),
73        }
74    }
75}
76
77/// A type erased version of [`DomainConstraint`] that we can reference on a [`VarDefinition`].
78///
79/// [`VarDefinition`]: crate::session::vars::definitions::VarDefinition
80pub trait DynDomainConstraint: Debug + Send + Sync + 'static {
81    fn check(&self, var: &dyn Var, v: &dyn Value) -> Result<(), VarError>;
82    fn describe(&self) -> DomainDescription;
83}
84
85impl<D> DynDomainConstraint for D
86where
87    D: DomainConstraint + Send + Sync + 'static,
88    D::Value: Value,
89{
90    fn check(&self, var: &dyn Var, v: &dyn Value) -> Result<(), VarError> {
91        let val = v
92            .as_any()
93            .downcast_ref::<D::Value>()
94            .expect("type should match");
95        self.check(var, val)
96    }
97
98    fn describe(&self) -> DomainDescription {
99        DomainConstraint::describe(self)
100    }
101}
102
103pub trait DomainConstraint: Debug + Send + Sync + 'static {
104    type Value;
105
106    fn check(&self, var: &dyn Var, v: &Self::Value) -> Result<(), VarError>;
107
108    /// Describes the accepted values, for tools that validate a parameter's
109    /// value without running [`Self::check`].
110    fn describe(&self) -> DomainDescription;
111}
112
113/// The values a [`DomainConstraint`] accepts.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum DomainDescription {
116    /// Rejects negative and NaN values.
117    NonNegativeNonNan,
118    /// Rejects zero.
119    NonZero,
120    /// Accepts values within the bounds, each formatted with [`Value::format`].
121    Range {
122        start: Bound<String>,
123        end: Bound<String>,
124    },
125}
126
127impl DomainDescription {
128    fn range<T: Value>(range: &impl RangeBounds<T>) -> Self {
129        DomainDescription::Range {
130            start: range.start_bound().map(Value::format),
131            end: range.end_bound().map(Value::format),
132        }
133    }
134}
135
136#[derive(Debug, Clone, Eq, PartialEq)]
137pub struct NumericNonNegNonNan;
138
139impl DomainConstraint for NumericNonNegNonNan {
140    type Value = Numeric;
141
142    fn describe(&self) -> DomainDescription {
143        DomainDescription::NonNegativeNonNan
144    }
145
146    fn check(&self, var: &dyn Var, n: &Numeric) -> Result<(), VarError> {
147        if n.is_nan() || n.is_negative() {
148            Err(VarError::InvalidParameterValue {
149                name: var.name(),
150                invalid_values: vec![n.to_string()],
151                reason: "only supports non-negative, non-NaN numeric values".to_string(),
152            })
153        } else {
154            Ok(())
155        }
156    }
157}
158
159#[derive(Debug, Clone, Eq, PartialEq)]
160pub struct NonZeroDuration;
161
162impl DomainConstraint for NonZeroDuration {
163    type Value = Duration;
164
165    fn describe(&self) -> DomainDescription {
166        DomainDescription::NonZero
167    }
168
169    fn check(&self, var: &dyn Var, d: &Duration) -> Result<(), VarError> {
170        if d.is_zero() {
171            Err(VarError::InvalidParameterValue {
172                name: var.name(),
173                invalid_values: vec![format!("{:?}", d)],
174                reason: "only supports non-zero durations".to_string(),
175            })
176        } else {
177            Ok(())
178        }
179    }
180}
181
182#[derive(Debug, Clone, Eq, PartialEq)]
183pub struct NumericInRange<R>(pub R);
184
185impl<R> DomainConstraint for NumericInRange<R>
186where
187    R: RangeBounds<f64> + std::fmt::Debug + Send + Sync + 'static,
188{
189    type Value = Numeric;
190
191    fn describe(&self) -> DomainDescription {
192        DomainDescription::range(&self.0)
193    }
194
195    fn check(&self, var: &dyn Var, n: &Numeric) -> Result<(), VarError> {
196        let n: f64 = (*n)
197            .try_into()
198            .map_err(|_e| VarError::InvalidParameterValue {
199                name: var.name(),
200                invalid_values: vec![n.to_string()],
201                // This first check can fail if the value is NaN, out of range,
202                // OR if it underflows (i.e. is very close to 0 without actually being 0, and the closest
203                // representable float is 0).
204                //
205                // The underflow case is very unlikely to be accidentally hit by a user, so let's
206                // not make the error message more confusing by talking about it, even though that makes
207                // the error message slightly inaccurate.
208                //
209                // If the user tries to set the paramater to 0.000<hundreds more zeros>001
210                // and gets the message "only supports values in range [0.0..=1.0]", I think they will
211                // understand, or at least accept, what's going on.
212                reason: format!("only supports values in range {:?}", self.0),
213            })?;
214        if !self.0.contains(&n) {
215            Err(VarError::InvalidParameterValue {
216                name: var.name(),
217                invalid_values: vec![n.to_string()],
218                reason: format!("only supports values in range {:?}", self.0),
219            })
220        } else {
221            Ok(())
222        }
223    }
224}
225
226#[derive(Debug, Clone, Eq, PartialEq)]
227pub struct ByteSizeInRange<R>(pub R);
228
229impl<R> DomainConstraint for ByteSizeInRange<R>
230where
231    R: RangeBounds<ByteSize> + std::fmt::Debug + Send + Sync + 'static,
232{
233    type Value = ByteSize;
234
235    fn describe(&self) -> DomainDescription {
236        DomainDescription::range(&self.0)
237    }
238
239    fn check(&self, var: &dyn Var, size: &ByteSize) -> Result<(), VarError> {
240        if self.0.contains(size) {
241            Ok(())
242        } else {
243            Err(VarError::InvalidParameterValue {
244                name: var.name(),
245                invalid_values: vec![size.to_string()],
246                reason: format!("only supports values in range {:?}", self.0),
247            })
248        }
249    }
250}
251
252#[derive(Debug, Clone, Eq, PartialEq)]
253pub struct U32InRange<R>(pub R);
254
255impl<R> DomainConstraint for U32InRange<R>
256where
257    R: RangeBounds<u32> + std::fmt::Debug + Send + Sync + 'static,
258{
259    type Value = u32;
260
261    fn describe(&self) -> DomainDescription {
262        DomainDescription::range(&self.0)
263    }
264
265    fn check(&self, var: &dyn Var, n: &u32) -> Result<(), VarError> {
266        if self.0.contains(n) {
267            Ok(())
268        } else {
269            Err(VarError::InvalidParameterValue {
270                name: var.name(),
271                invalid_values: vec![n.to_string()],
272                reason: format!("only supports values in range {:?}", self.0),
273            })
274        }
275    }
276}