Files
2026-07-13 12:32:25 +08:00

287 lines
9.6 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"os"
"regexp"
"strings"
"time"
"github.com/dolthub/doltgresql/core"
cerrors "github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/variables"
"gopkg.in/src-d/go-errors.v1"
)
// doltConfigParameters is a list of Dolt-specific configuration parameters that can be used in SET statement.
var doltConfigParameters = make(map[string]sql.SystemVariable)
// Init initializes or appends to SystemVariables as it functions as a global variable.
// Currently, we append all of postgres configuration parameters to sql.SystemVariables.
// This means that all of mysql system variables and postgres config parameters will be stored together.
// TODO: get rid of me, use an integration point to define new sysvars
func Init() {
// There are two postgres parameters have the same name as mysql variables
// TODO: issue with this approach is that those parameters will override the mysql variables.
if sql.SystemVariables == nil {
// unlikely this would happen since init() in gms package is executed first
variables.InitSystemVariables()
}
// Regression tests assume the Los Angeles timezone, so we'll set that here if the env var is set
if _, ok := os.LookupEnv("REGRESSION_TESTING"); ok {
timezone := postgresConfigParameters["timezone"].(*Parameter)
timezone.Default = "America/Los_Angeles"
timezone.ResetVal = "America/Los_Angeles"
}
params := make([]sql.SystemVariable, len(postgresConfigParameters))
i := 0
for _, sysVar := range postgresConfigParameters {
params[i] = sysVar
i++
}
for _, doltSysVar := range sqle.DoltSystemVariables {
doltConfigParameters[doltSysVar.GetName()] = doltSysVar
}
sql.SystemVariables.AddSystemVariables(params)
sqle.AddDoltSystemVariables()
}
var (
ErrInvalidValue = errors.NewKind("ERROR: invalid value for parameter \"%s\": \"%s\"")
ErrCannotChangeAtRuntime = errors.NewKind("ERROR: parameter \"%s\" cannot be changed now")
)
var _ sql.SystemVariable = (*Parameter)(nil)
type Parameter struct {
Name string
Default any
Category string
ShortDesc string
Context ParameterContext
Type sql.Type
Source ParameterSource
ResetVal any
Scope sql.SystemVariableScope
ValidateFunc func(currVal, newVal any) (any, bool)
}
// GetName implements sql.SystemVariable.
func (p *Parameter) GetName() string {
return p.Name
}
// GetType implements sql.SystemVariable.
func (p *Parameter) GetType() sql.Type {
return p.Type
}
// GetSessionScope implements sql.SystemVariable.
func (p *Parameter) GetSessionScope() sql.SystemVariableScope {
return GetPgsqlScope(PsqlScopeSession)
}
// SetDefault implements sql.SystemVariable.
func (p *Parameter) SetDefault(a any) {
if validatedVal, ok := p.ValidateFunc(p.Default, a); ok {
p.Default = validatedVal
}
}
// GetDefault implements sql.SystemVariable.
func (p *Parameter) GetDefault() any {
return p.Default
}
// InitValue implements sql.SystemVariable.
func (p *Parameter) InitValue(ctx *sql.Context, val any, global bool) (sql.SystemVarValue, error) {
convertedVal, _, err := p.Type.Convert(ctx, val)
if err != nil {
return sql.SystemVarValue{}, err
}
currVal, err := ctx.GetSessionVariable(ctx, p.Name)
if err != nil {
return sql.SystemVarValue{}, err
}
if p.ValidateFunc != nil {
v, ok := p.ValidateFunc(currVal, convertedVal)
if !ok {
return sql.SystemVarValue{}, ErrInvalidValue.New(p.Name, convertedVal)
}
convertedVal = v
}
svv := sql.SystemVarValue{
Var: p,
Val: convertedVal,
}
return svv, nil
}
// SetValue implements sql.SystemVariable.
func (p *Parameter) SetValue(ctx *sql.Context, val any, global bool) (sql.SystemVarValue, error) {
if p.IsReadOnly() {
return sql.SystemVarValue{}, ErrCannotChangeAtRuntime.New(p.Name)
}
// TODO: Do parsing of units for memory and time parameters
return p.InitValue(ctx, val, global)
}
// IsReadOnly implements sql.SystemVariable.
func (p *Parameter) IsReadOnly() bool {
switch strings.ToLower(p.Name) {
case "server_version", "server_encoding", "lc_collate", "lc_ctype", "is_superuser":
return true
}
switch p.Context {
case ParameterContextInternal, ParameterContextPostmaster, ParameterContextSighup,
ParameterContextSuperUserBackend, ParameterContextBackend:
// Read the docs above the ParameterContext
// TODO: some of above contexts need support, return error for now
return true
case ParameterContextSuperUser, ParameterContextUser:
// TODO: need to check for 'superuser' and appropriate 'SET' privileges.
// Can be set from `postgresql.conf`, or within a session via the `SET` command.
return false
}
return false
}
// IsGlobalOnly implements sql.SystemVariable.
func (p *Parameter) IsGlobalOnly() bool {
return false
}
// DisplayString implements sql.SystemVariable.
func (p *Parameter) DisplayString(_ string) string {
return p.Name
}
// ParameterContext sets level of difficulty of changing the parameter settings.
// For more detailed description on how to change the settings of specific context,
// https://www.postgresql.org/docs/current/view-pg-settings.html
type ParameterContext string
// The following constants are in order of decreasing difficulty of changing the setting.
const (
ParameterContextInternal ParameterContext = "internal"
ParameterContextPostmaster ParameterContext = "postmaster"
ParameterContextSighup ParameterContext = "sighup"
ParameterContextSuperUserBackend ParameterContext = "superuser-backend"
ParameterContextBackend ParameterContext = "backend"
ParameterContextSuperUser ParameterContext = "superuser"
ParameterContextUser ParameterContext = "user"
)
// ParameterSource sets the source of the current parameter value.
type ParameterSource string
const (
ParameterSourceClient ParameterSource = "client"
// ParameterSourceConfigurationFile means that the parameter needs to set
// its Default and ResetVal to what's defined in the given config file.
ParameterSourceConfigurationFile ParameterSource = "configuration file"
ParameterSourceDefault ParameterSource = "default"
// ParameterSourceOverride means the default and reset value needs to be set at server start time
// TODO: currently the default and reset values are dummy values.
ParameterSourceOverride ParameterSource = "override"
)
var _ sql.SystemVariableScope = (*PgsqlScope)(nil)
// PgsqlScope represents the scope of a PostgreSQL configuration parameter.
type PgsqlScope struct {
Type PgsqlScopeType
}
func GetPgsqlScope(t PgsqlScopeType) sql.SystemVariableScope {
return &PgsqlScope{Type: t}
}
// SetValue implements sql.SystemVariableScope.
func (p *PgsqlScope) SetValue(ctx *sql.Context, name string, val any) error {
switch p.Type {
case PsqlScopeSession:
// Reset any cached variables in ContextValues
// TODO: this may negatively impact performance for SET expressions
_ = core.SetDateStyleOutputFormat(ctx, "")
err := ctx.SetSessionVariable(ctx, name, val)
return err
case PsqlScopeLocal:
// TODO: support LOCAL scope
return cerrors.Errorf("unsupported scope `%v` on configuration parameter `%s`", p.Type, name)
default:
return cerrors.Errorf("unable to set `%s` due to unknown scope `%v`", name, p.Type)
}
}
// GetValue implements sql.SystemVariableScope.
func (p *PgsqlScope) GetValue(ctx *sql.Context, name string, _ sql.CollationID) (any, error) {
switch p.Type {
case PsqlScopeSession:
val, err := ctx.GetSessionVariable(ctx, name)
if err != nil {
return nil, err
}
return val, nil
case PsqlScopeLocal:
// TODO: support LOCAL scope
return nil, cerrors.Errorf("unsupported scope `%v` on configuration parameter `%s`", p.Type, name)
default:
return nil, cerrors.Errorf("unknown scope `%v` on configuration parameter `%s`", p.Type, name)
}
}
// IsGlobalOnly implements sql.SystemVariableScope.
func (p *PgsqlScope) IsGlobalOnly() bool {
// In Postgres, there is no GLOBAL scope.
return false
}
// IsSessionOnly implements sql.SystemVariableScope.
func (p *PgsqlScope) IsSessionOnly() bool {
return p.Type == PsqlScopeSession
}
// PgsqlScopeType represents the scope of a configuration parameter.
type PgsqlScopeType byte
const (
// PsqlScopeSession is set when the configuration parameter exists only in the session context.
PsqlScopeSession PgsqlScopeType = iota
// PsqlScopeLocal is set when the configuration parameter exists only in the local context.
PsqlScopeLocal
)
// tzOffsetRegex is a regex for matching timezone offsets (e.g. +01:00).
var tzOffsetRegex = regexp.MustCompile(`(?m)^([+\-])(\d{2}):(\d{2})$`)
// TzOffsetToDuration takes in a timezone offset (e.g. "+01:00") and returns it as a time.Duration.
// If any problems are encountered, an error is returned.
func TzOffsetToDuration(d string) (time.Duration, error) {
matches := tzOffsetRegex.FindStringSubmatch(d)
if len(matches) == 4 {
symbol := matches[1]
hours := matches[2]
mins := matches[3]
return time.ParseDuration(symbol + hours + "h" + mins + "m")
} else {
return -1, cerrors.Errorf("error: unable to process time")
}
}