Skip to content

Commit 3d8a9f0

Browse files
committed
server: extend SQL error messages by regex config
1 parent e44fadc commit 3d8a9f0

10 files changed

Lines changed: 323 additions & 1 deletion

File tree

pkg/config/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ go_test(
4242
data = glob(["**"]),
4343
embed = [":config"],
4444
flaky = True,
45-
shard_count = 32,
45+
shard_count = 34,
4646
deps = [
4747
"//pkg/config/deploymode",
4848
"//pkg/config/kerneltype",

pkg/config/config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"os"
2525
"os/user"
2626
"path/filepath"
27+
"regexp"
2728
"sort"
2829
"strings"
2930
"sync"
@@ -261,6 +262,8 @@ type Config struct {
261262
// 2. 'zone' is a special key that indicates the DC location of this tidb-server. If it is set, the value for this
262263
// key will be the default value of the session variable `txn_scope` for this tidb-server.
263264
Labels map[string]string `toml:"labels" json:"labels"`
265+
// ExtendedErrorMsgs maps error message regexps to configured suffixes for selected user-facing errors.
266+
ExtendedErrorMsgs map[string]string `toml:"extended-error-msgs" json:"extended-error-msgs"`
264267

265268
// EnableGlobalIndex is deprecated.
266269
EnableGlobalIndex bool `toml:"enable-global-index" json:"enable-global-index"`
@@ -1175,6 +1178,7 @@ var defaultConf = Config{
11751178
EnableCollectExecutionInfo: true,
11761179
EnableTelemetry: false,
11771180
Labels: make(map[string]string),
1181+
ExtendedErrorMsgs: make(map[string]string),
11781182
EnableGlobalIndex: false,
11791183
Security: Security{
11801184
SpilledFileEncryptionMethod: SpilledFileEncryptionMethodPlaintext,
@@ -1214,9 +1218,21 @@ var (
12141218
// NewConfig creates a new config instance with default value.
12151219
func NewConfig() *Config {
12161220
conf := defaultConf
1221+
conf.ExtendedErrorMsgs = cloneStringMap(defaultConf.ExtendedErrorMsgs)
12171222
return &conf
12181223
}
12191224

1225+
func cloneStringMap(src map[string]string) map[string]string {
1226+
if src == nil {
1227+
return nil
1228+
}
1229+
dst := make(map[string]string, len(src))
1230+
for k, v := range src {
1231+
dst[k] = v
1232+
}
1233+
return dst
1234+
}
1235+
12201236
// GetGlobalConfig returns the global configuration for this server.
12211237
// It should store configuration from command line and configuration file.
12221238
// Other parts of the system can read the global configuration use this function.
@@ -1464,6 +1480,11 @@ func (c *Config) Valid() error {
14641480
if c.Security.SkipGrantTable && !hasRootPrivilege() {
14651481
return fmt.Errorf("TiDB run with skip-grant-table need root privilege")
14661482
}
1483+
for pattern := range c.ExtendedErrorMsgs {
1484+
if _, err := regexp.Compile(pattern); err != nil {
1485+
return fmt.Errorf("invalid extended-error-msgs regexp %q: %w", pattern, err)
1486+
}
1487+
}
14671488
if !c.Store.Valid() {
14681489
return fmt.Errorf("invalid store=%s, valid storages=%v", c.Store, StoreTypeList())
14691490
}
@@ -1633,6 +1654,7 @@ func init() {
16331654

16341655
func initByLDFlags(edition, checkBeforeDropLDFlag string) {
16351656
conf := defaultConf
1657+
conf.ExtendedErrorMsgs = cloneStringMap(defaultConf.ExtendedErrorMsgs)
16361658
if intest.InTest && kerneltype.IsNextGen() {
16371659
// In test mode, without reading a config file, we still assume the `GetGlobalConfig()` returns
16381660
// a valid config file. However, the "valid" nextgen config file should always have a keyspace name.

pkg/config/config.toml.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,11 @@ tikv-raftstore-store-write-trigger-wb-bytes = 0.00006100
480480
tikv-storage-processed-keys-batch-get = 0.00266791
481481
tikv-storage-processed-keys-get = 0.01416829
482482

483+
# extended-error-msgs appends configured messages to SQL errors matching regexps. When multiple
484+
# regexps match the same error, TiDB uses the longest regexp pattern first.
485+
# Example: "^Feature '.+' is not supported$" = "see the feature limitations for more details"
486+
[extended-error-msgs]
487+
483488
# instance scope variables
484489
# These options are also available as a system variable for online configuration
485490
# changes to the system variable do not persist to the cluster. You must make changes

pkg/config/config.toml.nextgen.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,11 @@ allow-expression-index = false
446446
# engines means allow the tidb server read data from which types of engines. options: "tikv", "tiflash", "tidb".
447447
engines = ["tikv", "tiflash", "tidb"]
448448

449+
# extended-error-msgs appends configured messages to SQL errors matching regexps. When multiple
450+
# regexps match the same error, TiDB uses the longest regexp pattern first.
451+
# Example: "^Feature '.+' is not supported$" = "see the feature limitations for more details"
452+
[extended-error-msgs]
453+
449454
# instance scope variables
450455
# These options are also available as a system variable for online configuration
451456
# changes to the system variable do not persist to the cluster. You must make changes

pkg/config/config_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,41 @@ disable-error-stack = false
170170
`, nbFalse, nbUnset, nbUnset, nbUnset, false, true)
171171
}
172172

173+
func TestExtendedErrorMsgsConfig(t *testing.T) {
174+
configFile := filepath.Join(t.TempDir(), "config.toml")
175+
require.NoError(t, os.WriteFile(configFile, []byte(`
176+
[extended-error-msgs]
177+
"^Access denied for user '.+'@'.+' \\(using password: (YES|NO)\\)$" = "see https://docs.pingcap.com/tidbcloud/select-cluster-tier#user-name-prefix for more details"
178+
"^require_secure_transport can not be set to ON with SEM\\(security enhanced mode\\) enabled$" = "see https://docs.pingcap.com/tidbcloud/secure-connections-to-serverless-tier-clusters for more details"
179+
"^sleep\\(\\) argument is greater than [0-9]+$" = "see https://docs.pingcap.com/tidbcloud/serverless-tier-limitations#sql for more details"
180+
"^[A-Z ]+ command denied to user '.+'@'.+' for table '.+'$" = "see https://docs.pingcap.com/tidbcloud/limited-sql-features#system-tables for more details"
181+
"^Access denied; you need \\(at least one of\\) the RESTRICTED_VARIABLES_ADMIN privilege\\(s\\) for this operation$" = "see https://docs.pingcap.com/tidbcloud/limited-sql-features#system-variables for more details"
182+
"^Feature '.+' is not supported when security enhanced mode is enabled$" = "see https://docs.pingcap.com/tidbcloud/limited-sql-features#statements for more details"
183+
`), 0644))
184+
185+
conf := NewConfig()
186+
require.NoError(t, conf.Load(configFile))
187+
require.NoError(t, conf.Valid())
188+
require.Equal(t, map[string]string{
189+
`^Access denied for user '.+'@'.+' \(using password: (YES|NO)\)$`: "see https://docs.pingcap.com/tidbcloud/select-cluster-tier#user-name-prefix for more details",
190+
`^require_secure_transport can not be set to ON with SEM\(security enhanced mode\) enabled$`: "see https://docs.pingcap.com/tidbcloud/secure-connections-to-serverless-tier-clusters for more details",
191+
`^sleep\(\) argument is greater than [0-9]+$`: "see https://docs.pingcap.com/tidbcloud/serverless-tier-limitations#sql for more details",
192+
`^[A-Z ]+ command denied to user '.+'@'.+' for table '.+'$`: "see https://docs.pingcap.com/tidbcloud/limited-sql-features#system-tables for more details",
193+
`^Access denied; you need \(at least one of\) the RESTRICTED_VARIABLES_ADMIN privilege\(s\) for this operation$`: "see https://docs.pingcap.com/tidbcloud/limited-sql-features#system-variables for more details",
194+
`^Feature '.+' is not supported when security enhanced mode is enabled$`: "see https://docs.pingcap.com/tidbcloud/limited-sql-features#statements for more details",
195+
}, conf.ExtendedErrorMsgs)
196+
197+
require.Empty(t, NewConfig().ExtendedErrorMsgs)
198+
}
199+
200+
func TestExtendedErrorMsgsInvalidRegexp(t *testing.T) {
201+
conf := NewConfig()
202+
conf.ExtendedErrorMsgs = map[string]string{
203+
"[": "invalid regexp",
204+
}
205+
require.ErrorContains(t, conf.Valid(), "invalid extended-error-msgs regexp")
206+
}
207+
173208
func TestRemovedVariableCheck(t *testing.T) {
174209
configTest := []struct {
175210
options string

pkg/server/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ go_library(
9393
"//pkg/util/cpuprofile",
9494
"//pkg/util/dbterror",
9595
"//pkg/util/dbterror/exeerrors",
96+
"//pkg/util/errmsg",
9697
"//pkg/util/execdetails",
9798
"//pkg/util/fastrand",
9899
"//pkg/util/hack",

pkg/server/conn.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import (
103103
"github.com/pingcap/tidb/pkg/util/chunk"
104104
contextutil "github.com/pingcap/tidb/pkg/util/context"
105105
"github.com/pingcap/tidb/pkg/util/dbterror/exeerrors"
106+
"github.com/pingcap/tidb/pkg/util/errmsg"
106107
"github.com/pingcap/tidb/pkg/util/execdetails"
107108
"github.com/pingcap/tidb/pkg/util/hack"
108109
"github.com/pingcap/tidb/pkg/util/intest"
@@ -1637,6 +1638,7 @@ func (cc *clientConn) writeError(ctx context.Context, e error) error {
16371638
m = mysql.NewErrf(mysql.ErrUnknown, "%s", nil, e.Error())
16381639
}
16391640
}
1641+
errmsg.ExtendErrorMessage(m)
16401642

16411643
cc.lastCode = m.Code
16421644
defer errno.IncrementError(m.Code, cc.user, cc.peerHost)

pkg/util/errmsg/BUILD.bazel

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "errmsg",
5+
srcs = ["errmsg.go"],
6+
importpath = "github.com/pingcap/tidb/pkg/util/errmsg",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//pkg/config",
10+
"//pkg/parser/mysql",
11+
],
12+
)
13+
14+
go_test(
15+
name = "errmsg_test",
16+
timeout = "short",
17+
srcs = ["errmsg_test.go"],
18+
embed = [":errmsg"],
19+
flaky = True,
20+
deps = [
21+
"//pkg/config",
22+
"//pkg/parser/mysql",
23+
"@com_github_stretchr_testify//require",
24+
],
25+
)

pkg/util/errmsg/errmsg.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2026 PingCAP, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package errmsg
16+
17+
import (
18+
"fmt"
19+
"regexp"
20+
"sort"
21+
"strings"
22+
"sync"
23+
24+
"github.com/pingcap/tidb/pkg/config"
25+
"github.com/pingcap/tidb/pkg/parser/mysql"
26+
)
27+
28+
var regexpCache sync.Map
29+
30+
// ExtendErrorMessage appends a configured message suffix to selected SQL errors.
31+
func ExtendErrorMessage(m *mysql.SQLError) {
32+
if m == nil {
33+
return
34+
}
35+
36+
extendedMsgs := config.GetGlobalConfig().ExtendedErrorMsgs
37+
if len(extendedMsgs) == 0 {
38+
return
39+
}
40+
41+
patterns := make([]string, 0, len(extendedMsgs))
42+
for pattern := range extendedMsgs {
43+
patterns = append(patterns, pattern)
44+
}
45+
sort.Slice(patterns, func(i, j int) bool {
46+
if len(patterns[i]) != len(patterns[j]) {
47+
return len(patterns[i]) > len(patterns[j])
48+
}
49+
return patterns[i] < patterns[j]
50+
})
51+
52+
for _, pattern := range patterns {
53+
extendedMsg := extendedMsgs[pattern]
54+
if extendedMsg == "" {
55+
continue
56+
}
57+
re, err := compileRegexp(pattern)
58+
if err != nil {
59+
continue
60+
}
61+
if re.MatchString(m.Message) {
62+
extendErrorMessage(m, extendedMsg)
63+
return
64+
}
65+
}
66+
}
67+
68+
func compileRegexp(pattern string) (*regexp.Regexp, error) {
69+
cachedRegexp, ok := regexpCache.Load(pattern)
70+
if ok {
71+
return cachedRegexp.(*regexp.Regexp), nil
72+
}
73+
74+
compiledRegexp, err := regexp.Compile(pattern)
75+
if err != nil {
76+
return nil, err
77+
}
78+
actual, _ := regexpCache.LoadOrStore(pattern, compiledRegexp)
79+
return actual.(*regexp.Regexp), nil
80+
}
81+
82+
func extendErrorMessage(m *mysql.SQLError, msg string) {
83+
m.Message = fmt.Sprintf("%s, %s.", strings.TrimSuffix(m.Message, "."), strings.TrimSuffix(msg, "."))
84+
}

0 commit comments

Comments
 (0)