Skip to content

Commit c23bfbe

Browse files
committed
settings: support enum cluster setting validation
Previously, enum cluster settings did not support validation check. This commit adds one. I considered whether validation should take a string or an enum. If we validate using the enum directly, we’d need separate validator function signatures for each enum type, since `EnumSetting[T constraints.Integer]` is generic but `SettingOption` isn’t. If we take a string, `ParseEnum` happens earlier and `SET` only sees the enum's integer value, validating directly would require converting the int back to a string. I ended up taking in a string since it aligns with the existing design pattern around enum settings. 1. `RegisterEnumSetting` uses strings for default values. 2. It feels more intuitive to set the cluster setting with the string input. It did require us adding `parseEnumInt64`, which converts the enum's integer value back to its string representation for validation. Epic: none Release note: none
1 parent 97b5668 commit c23bfbe

File tree

4 files changed

+172
-3
lines changed

4 files changed

+172
-3
lines changed

pkg/settings/enum.go

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strconv"
1414
"strings"
1515

16+
"github.com/cockroachdb/errors"
1617
"golang.org/x/exp/constraints"
1718
)
1819

@@ -22,13 +23,17 @@ type EnumSetting[T constraints.Integer] struct {
2223
defaultValue T
2324
// enumValues maps each valid value of T to a lowercase string.
2425
enumValues map[T]string
26+
validateFn func(string) error
2527
}
2628

2729
// AnyEnumSetting is an interface that is used when the specific enum type T is
2830
// not known.
2931
type AnyEnumSetting interface {
3032
internalSetting
3133

34+
// Validate validates the enum value.
35+
Validate(value int64) error
36+
3237
// ParseEnum parses a string that is either a string in enumValues or an
3338
// integer and returns the enum value as an int64 (and a boolean that
3439
// indicates if it was parseable).
@@ -85,18 +90,32 @@ func (e *EnumSetting[T]) setToDefault(ctx context.Context, sv *Values) {
8590
// See if the default value was overridden.
8691
if val := sv.getDefaultOverride(e.slot); val != nil {
8792
// As per the semantics of override, these values don't go through
88-
// validation.
89-
sv.setInt64(ctx, e.slot, val.(int64))
93+
// validation. TODO(wenyihu6): there seems to be a bug here. See more in
94+
// #149982.
95+
_ = e.set(ctx, sv, val.(int64))
9096
return
9197
}
92-
sv.setInt64(ctx, e.slot, int64(e.defaultValue))
98+
if err := e.set(ctx, sv, int64(e.defaultValue)); err != nil {
99+
panic(err)
100+
}
101+
}
102+
103+
func (e *EnumSetting[T]) set(ctx context.Context, sv *Values, v int64) error {
104+
if err := e.Validate(v); err != nil {
105+
return err
106+
}
107+
sv.setInt64(ctx, e.slot, v)
108+
return nil
93109
}
94110

95111
func (e *EnumSetting[T]) decodeAndSet(ctx context.Context, sv *Values, encoded string) error {
96112
v, err := strconv.ParseInt(encoded, 10, 64)
97113
if err != nil {
98114
return err
99115
}
116+
if err := e.Validate(v); err != nil {
117+
return err
118+
}
100119
sv.setInt64(ctx, e.slot, v)
101120
return nil
102121
}
@@ -124,6 +143,26 @@ func (e *EnumSetting[T]) DecodeToString(encoded string) (string, error) {
124143
return encoded, nil
125144
}
126145

146+
// parseEnumInt64 converts an int64 to its enum string value for validation.
147+
// The string value is used by validateFn to validate the enum.
148+
func (e *EnumSetting[T]) parseEnumInt64(value int64) (string, bool) {
149+
for k, v := range e.enumValues {
150+
if value == int64(k) {
151+
return v, true
152+
}
153+
}
154+
return "", false
155+
}
156+
157+
// Validate validates the enum value.
158+
func (e *EnumSetting[T]) Validate(value int64) error {
159+
parsed, ok := e.parseEnumInt64(value)
160+
if !ok {
161+
return fmt.Errorf("invalid enum value: %d", value)
162+
}
163+
return e.validateFn(parsed)
164+
}
165+
127166
// ParseEnum parses a string that is either a string in enumValues or an integer
128167
// and returns the enum value as an int64 (and a boolean that indicates if it
129168
// was parseable).
@@ -212,6 +251,22 @@ func RegisterEnumSetting[T constraints.Integer](
212251
enumValuesLower[k] = strings.ToLower(v)
213252
}
214253

254+
validateFn := func(val string) error {
255+
for _, opt := range opts {
256+
switch {
257+
case opt.commonOpt != nil:
258+
continue
259+
case opt.validateEnumFn != nil:
260+
default:
261+
panic(errors.AssertionFailedf("wrong validator type"))
262+
}
263+
if err := opt.validateEnumFn(val); err != nil {
264+
return err
265+
}
266+
}
267+
return nil
268+
}
269+
215270
defaultVal := func() T {
216271
for k, v := range enumValues {
217272
if v == defaultValue {
@@ -221,9 +276,14 @@ func RegisterEnumSetting[T constraints.Integer](
221276
panic(fmt.Sprintf("enum registered with default value %s not in map %s", defaultValue, enumValuesToDesc(enumValuesLower)))
222277
}()
223278

279+
if err := validateFn(defaultValue); err != nil {
280+
panic(errors.Wrap(err, "invalid default"))
281+
}
282+
224283
setting := &EnumSetting[T]{
225284
defaultValue: defaultVal,
226285
enumValues: enumValuesLower,
286+
validateFn: validateFn,
227287
}
228288

229289
register(class, key, fmt.Sprintf("%s %s", desc, enumValuesToDesc(enumValues)), setting)

pkg/settings/options.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type SettingOption struct {
2020
validateFloat64Fn func(float64) error
2121
validateStringFn func(*Values, string) error
2222
validateProtoFn func(*Values, protoutil.Message) error
23+
validateEnumFn func(string) error
2324
}
2425

2526
// NameStatus indicates the status of a setting name.
@@ -119,6 +120,11 @@ func WithValidateProto(fn func(*Values, protoutil.Message) error) SettingOption
119120
return SettingOption{validateProtoFn: fn}
120121
}
121122

123+
// WithValidateEnum adds a validation function for an enum setting.
124+
func WithValidateEnum(fn func(string) error) SettingOption {
125+
return SettingOption{validateEnumFn: fn}
126+
}
127+
122128
func (c *common) apply(opts []SettingOption) {
123129
for _, opt := range opts {
124130
if opt.commonOpt != nil {

pkg/settings/validation_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
package settings_test
77

88
import (
9+
"context"
910
"fmt"
1011
"strconv"
1112
"testing"
1213
"time"
1314

1415
"github.com/cockroachdb/cockroach/pkg/settings"
1516
"github.com/cockroachdb/cockroach/pkg/testutils"
17+
"github.com/cockroachdb/errors"
18+
"github.com/stretchr/testify/require"
1619
)
1720

1821
var cantBeTrue = settings.WithValidateBool(func(sv *settings.Values, b bool) error {
@@ -184,6 +187,70 @@ func TestValidationOptions(t *testing.T) {
184187
{val: 21, opt: settings.IntInRangeOrZeroDisable(10, 20), expectedErr: `expected value in range \[10, 20\] or 0 to disable, got: 21`},
185188
},
186189
},
190+
{
191+
testLabel: "int64 enum",
192+
settingFn: func(n int, ival interface{}, opt ...settings.SettingOption) settings.Setting {
193+
val := ival.(string)
194+
enumValues := map[int]string{
195+
0: "zero",
196+
1: "one",
197+
2: "two",
198+
}
199+
return settings.RegisterEnumSetting(
200+
settings.SystemOnly, settings.InternalKey(fmt.Sprintf("test-%d", n)),
201+
"desc", val, enumValues, opt...,
202+
)
203+
},
204+
subTests: []subTest{
205+
{val: "zero", opt: settings.WithValidateEnum(func(s string) error {
206+
if s == "zero" {
207+
return errors.New("cannot be zero")
208+
}
209+
return nil
210+
}), expectedErr: "cannot be zero"},
211+
{val: "one", opt: settings.WithValidateEnum(func(s string) error {
212+
if s == "zero" {
213+
return errors.New("cannot be zero")
214+
}
215+
return nil
216+
}), expectedErr: ""},
217+
{val: "two", opt: settings.WithValidateEnum(func(s string) error {
218+
return nil
219+
}), expectedErr: ""},
220+
},
221+
},
222+
{
223+
testLabel: "uint enum",
224+
settingFn: func(n int, ival interface{}, opt ...settings.SettingOption) settings.Setting {
225+
val := ival.(string)
226+
enumValues := map[uint]string{
227+
0: "zero",
228+
1: "one",
229+
2: "two",
230+
}
231+
return settings.RegisterEnumSetting(
232+
settings.SystemOnly, settings.InternalKey(fmt.Sprintf("test-%d", n)),
233+
"desc", val, enumValues, opt...,
234+
)
235+
},
236+
subTests: []subTest{
237+
{val: "zero", opt: settings.WithValidateEnum(func(s string) error {
238+
if s == "zero" {
239+
return errors.New("cannot be zero")
240+
}
241+
return nil
242+
}), expectedErr: "cannot be zero"},
243+
{val: "one", opt: settings.WithValidateEnum(func(s string) error {
244+
if s == "zero" {
245+
return errors.New("cannot be zero")
246+
}
247+
return nil
248+
}), expectedErr: ""},
249+
{val: "two", opt: settings.WithValidateEnum(func(s string) error {
250+
return nil
251+
}), expectedErr: ""},
252+
},
253+
},
187254
{
188255
testLabel: "bytesize",
189256
settingFn: func(n int, ival interface{}, opt ...settings.SettingOption) settings.Setting {
@@ -261,3 +328,33 @@ func TestValidationOptions(t *testing.T) {
261328
})
262329
}
263330
}
331+
332+
var enumWithValidation = settings.RegisterEnumSetting(settings.SystemVisible, "enum.with.validation", "desc", "one", map[int]string{
333+
0: "zero",
334+
1: "one",
335+
2: "two",
336+
}, settings.WithValidateEnum(func(v string) error {
337+
if v == "zero" {
338+
return errors.New("cannot be zero")
339+
}
340+
return nil
341+
}))
342+
343+
// TestEnumWithValidation tests that the enum setting with validation works as expected.
344+
func TestEnumWithValidation(t *testing.T) {
345+
ctx := context.Background()
346+
sv := &settings.Values{}
347+
sv.Init(ctx, settings.TestOpaque)
348+
u := settings.NewUpdater(sv)
349+
err := u.Set(ctx, "enum.with.validation", v(settings.EncodeInt(0), "e"))
350+
require.Error(t, err)
351+
require.ErrorContains(t, err, "cannot be zero")
352+
353+
err = u.Set(ctx, "enum.with.validation", v(settings.EncodeInt(2), "e"))
354+
require.NoError(t, err)
355+
require.Equal(t, 2, enumWithValidation.Get(sv))
356+
357+
err = u.SetToDefault(ctx, "enum.with.validation")
358+
require.NoError(t, err)
359+
require.Equal(t, 1, enumWithValidation.Get(sv))
360+
}

pkg/sql/set_cluster_setting.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,13 +820,19 @@ func toSettingString(
820820
if i, intOK := d.(*tree.DInt); intOK {
821821
v, ok := setting.ParseEnum(settings.EncodeInt(int64(*i)))
822822
if ok {
823+
if err := setting.Validate(v); err != nil {
824+
return "", err
825+
}
823826
return settings.EncodeInt(v), nil
824827
}
825828
return "", errors.WithHint(errors.Errorf("invalid integer value '%d' for enum setting", *i), setting.GetAvailableValuesAsHint())
826829
} else if s, ok := d.(*tree.DString); ok {
827830
str := string(*s)
828831
v, ok := setting.ParseEnum(str)
829832
if ok {
833+
if err := setting.Validate(v); err != nil {
834+
return "", err
835+
}
830836
return settings.EncodeInt(v), nil
831837
}
832838
return "", errors.WithHint(errors.Errorf("invalid string value '%s' for enum setting", str), setting.GetAvailableValuesAsHint())

0 commit comments

Comments
 (0)