/{go,integration-tests}: rebase main

This commit is contained in:
coffeegoddd
2025-08-25 15:53:15 -07:00
committed by coffeegoddd☕️✨
parent 4ae0bf48d7
commit 700e985978
10 changed files with 655 additions and 20 deletions
+196
View File
@@ -0,0 +1,196 @@
// Copyright 2025 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 sqlserver
import (
"context"
"net"
"strconv"
"github.com/sirupsen/logrus"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/sync/errgroup"
pkgmcp "github.com/dolthub/dolt-mcp/mcp/pkg"
mcpdb "github.com/dolthub/dolt-mcp/mcp/pkg/db"
"github.com/dolthub/dolt-mcp/mcp/pkg/toolsets"
"github.com/dolthub/dolt/go/libraries/utils/svcs"
)
// MCPConfig encapsulates MCP-specific configuration for the sql-server
type MCPConfig struct {
Port *int
User *string
Password *string
Database *string
}
// logrusZapCore implements a zapcore.Core that forwards entries to a logrus.Logger
type logrusZapCore struct {
l *logrus.Logger
prefix string
}
func newZapFromLogrusWithPrefix(l *logrus.Logger, prefix string) *zap.Logger {
core := &logrusZapCore{l: l, prefix: prefix}
return zap.New(core, zap.AddCallerSkip(1))
}
func (c *logrusZapCore) With(fields []zapcore.Field) zapcore.Core {
// zap will call Write with fields; we don't need to carry state here
return c
}
func (c *logrusZapCore) Enabled(lvl zapcore.Level) bool {
// Respect logrus current level
return zapToLogrusLevel(lvl) >= c.l.GetLevel()
}
func (c *logrusZapCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if c.Enabled(ent.Level) {
return ce.AddCore(ent, c)
}
return ce
}
func (c *logrusZapCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
enc := zapcore.NewMapObjectEncoder()
for _, f := range fields {
f.AddTo(enc)
}
// Build fields map
lf := logrus.Fields(enc.Fields)
msg := c.prefix + ent.Message
c.l.WithFields(lf).Log(zapToLogrusLevel(ent.Level), msg)
return nil
}
func (c *logrusZapCore) Sync() error { return nil }
func zapToLogrusLevel(l zapcore.Level) logrus.Level {
switch l {
case zapcore.DebugLevel:
return logrus.DebugLevel
case zapcore.InfoLevel:
return logrus.InfoLevel
case zapcore.WarnLevel:
return logrus.WarnLevel
case zapcore.ErrorLevel:
return logrus.ErrorLevel
case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel:
return logrus.FatalLevel
default:
return logrus.InfoLevel
}
}
// mcpInit validates and reserves the MCP port
func mcpInit(cfg *Config, state *svcs.ServiceState) func(context.Context) error {
return func(context.Context) error {
addr := net.JoinHostPort("0.0.0.0", strconv.Itoa(*cfg.MCP.Port))
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
_ = l.Close()
state.Swap(svcs.ServiceState_Init)
return nil
}
}
// mcpRun starts the MCP HTTP server and wires lifecycle to errgroup
func mcpRun(cfg *Config, lgr *logrus.Logger, state *svcs.ServiceState, cancelPtr *context.CancelFunc, groupPtr **errgroup.Group) func(context.Context) {
return func(ctx context.Context) {
if !state.CompareAndSwap(svcs.ServiceState_Init, svcs.ServiceState_Run) {
return
}
// Logger for MCP (prefix and level inherited from logrus)
logger := newZapFromLogrusWithPrefix(lgr, "[dolt-mcp] ")
if cfg.MCP.User == nil || *cfg.MCP.User == "" {
lgr.WithField("component", "dolt-mcp").Error("MCP user not defined")
return
}
// Build DB config
password := ""
if cfg.MCP.Password != nil {
password = *cfg.MCP.Password
}
dbName := ""
if cfg.MCP.Database != nil {
dbName = *cfg.MCP.Database
}
dbConf := mcpdb.Config{
Host: "127.0.0.1",
Port: cfg.ServerConfig.Port(),
User: *cfg.MCP.User,
Password: password,
DatabaseName: dbName,
}
srv, err := pkgmcp.NewMCPHTTPServer(
logger,
dbConf,
*cfg.MCP.Port,
toolsets.WithToolSet(&toolsets.PrimitiveToolSetV1{}),
)
if err != nil {
lgr.WithField("component", "dolt-mcp").Errorf("failed to start Dolt MCP HTTP server: %v", err)
return
}
runCtx, cancel := context.WithCancel(ctx)
*cancelPtr = cancel
g, gctx := errgroup.WithContext(runCtx)
g.Go(func() error { srv.ListenAndServe(gctx); return nil })
*groupPtr = g
}
}
// mcpStop gracefully stops the MCP server by cancelling context and waiting for the errgroup
func mcpStop(cancel context.CancelFunc, group *errgroup.Group, state *svcs.ServiceState) func() error {
return func() error {
if cancel != nil {
cancel()
}
if group != nil {
if err := group.Wait(); err != nil {
return err
}
}
state.Swap(svcs.ServiceState_Stopped)
return nil
}
}
// registerMCPService wires the MCP service into the controller using helper funcs
func registerMCPService(controller *svcs.Controller, cfg *Config, lgr *logrus.Logger) {
if cfg.MCP == nil || cfg.MCP.Port == nil || *cfg.MCP.Port <= 0 {
return
}
var state svcs.ServiceState
var cancel context.CancelFunc
var group *errgroup.Group
svc := &svcs.AnonService{
InitF: mcpInit(cfg, &state),
RunF: mcpRun(cfg, lgr, &state, &cancel, &group),
StopF: mcpStop(cancel, group, &state),
}
_ = controller.Register(svc)
}
@@ -0,0 +1,82 @@
// Copyright 2025 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 sqlserver
import (
"fmt"
"net"
"os"
"strconv"
"github.com/dolthub/dolt/go/libraries/doltcore/dconfig"
"github.com/dolthub/dolt/go/libraries/doltcore/servercfg"
)
// validateAndPrepareMCP performs coherence checks for MCP options and fails fast
// when the specified port is unavailable. It also sets DOLT_ROOT_HOST dynamically
// when MCP is enabled and the env var is not already set.
func validateAndPrepareMCP(
serverConfig servercfg.ServerConfig,
mcpPort *int,
mcpUser *string,
mcpPassword *string,
mcpDatabase *string,
) error {
// If any MCP-related arg is supplied without --mcp-port, error
if mcpPort == nil {
if mcpUser != nil && *mcpUser != "" {
return fmt.Errorf("--%s requires --%s to be set", mcpUserFlag, mcpPortFlag)
}
if mcpPassword != nil && *mcpPassword != "" {
return fmt.Errorf("--%s requires --%s to be set", mcpPasswordFlag, mcpPortFlag)
}
if mcpDatabase != nil && *mcpDatabase != "" {
return fmt.Errorf("--%s requires --%s to be set", mcpDatabaseFlag, mcpPortFlag)
}
return nil
}
// --mcp-user is REQUIRED when --mcp-port is provided
if mcpUser == nil || *mcpUser == "" {
return fmt.Errorf("--%s is required when --%s is specified", mcpUserFlag, mcpPortFlag)
}
// Range and conflict checks
if *mcpPort <= 0 || *mcpPort > 65535 {
return fmt.Errorf("invalid value for --%s '%d'", mcpPortFlag, *mcpPort)
}
if serverConfig.Port() == *mcpPort {
return fmt.Errorf("--%s must differ from --%s (both set to %d)", mcpPortFlag, portFlag, *mcpPort)
}
// If MCP is enabled and no explicit root host override exists, set DOLT_ROOT_HOST dynamically
// to the SQL listener host, defaulting to 127.0.0.1 when unspecified or wildcard.
if _, ok := os.LookupEnv(dconfig.EnvDoltRootHost); !ok {
hostForRoot := serverConfig.Host()
if hostForRoot == "" || hostForRoot == "0.0.0.0" || hostForRoot == "::" {
hostForRoot = "127.0.0.1"
}
_ = os.Setenv(dconfig.EnvDoltRootHost, hostForRoot)
}
// Preflight port availability to fail fast before starting controller
addr := net.JoinHostPort("0.0.0.0", strconv.Itoa(*mcpPort))
l, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("MCP port %d already in use: %v", *mcpPort, err)
}
_ = l.Close()
return nil
}
+4
View File
@@ -84,6 +84,7 @@ type Config struct {
Version string
Controller *svcs.Controller
ProtocolListenerFactory server.ProtocolListenerFunc
MCP *MCPConfig
}
// Serve starts a MySQL-compatible server. Returns any errors that were encountered.
@@ -866,6 +867,9 @@ func ConfigureServices(
},
}
controller.Register(RunSQLServer)
// Optionally start an MCP HTTP server
registerMCPService(controller, cfg, lgr)
}
// heartbeatService is a service that sends a heartbeat event to the metrics server once a day
@@ -56,6 +56,10 @@ const (
remotesapiReadOnlyFlag = "remotesapi-readonly"
goldenMysqlConn = "golden"
eventSchedulerStatus = "event-scheduler"
mcpPortFlag = "mcp-port"
mcpUserFlag = "mcp-user"
mcpPasswordFlag = "mcp-password"
mcpDatabaseFlag = "mcp-database"
)
func indentLines(s string) string {
@@ -195,6 +199,12 @@ func (cmd SqlServerCmd) ArgParserWithName(name string) *argparser.ArgParser {
ap.SupportsFlag(remotesapiReadOnlyFlag, "", "Disable writes to the sql-server via the push operations. SQL writes are unaffected by this setting.")
ap.SupportsString(goldenMysqlConn, "", "mysql connection string", "Provides a connection string to a MySQL instance to be used to validate query results")
ap.SupportsString(eventSchedulerStatus, "", "status", "Determines whether the Event Scheduler is enabled and running on the server. It has one of the following values: 'ON', 'OFF' or 'DISABLED'.")
// Start an MCP HTTP server connected to this sql-server on the given port
ap.SupportsUint(mcpPortFlag, "", "port", "If provided, runs a Dolt MCP HTTP server on this port alongside the sql-server.")
// MCP SQL credentials (user required when MCP enabled; password optional)
ap.SupportsString(mcpUserFlag, "", "user", "SQL user for MCP to connect as (required when --mcp-port is set).")
ap.SupportsString(mcpPasswordFlag, "", "password", "Optional SQL password for MCP to connect with (requires --mcp-user). Defaults to env DOLT_ROOT_PASSWORD if unset.")
ap.SupportsString(mcpDatabaseFlag, "", "database", "Optional SQL database name MCP should connect to (requires --mcp-port and --mcp-user).")
return ap
}
@@ -266,6 +276,37 @@ func StartServer(ctx context.Context, versionStr, commandStr string, args []stri
return err
}
// Optional MCP HTTP port
var mcpPortPtr *int
if mp, ok := apr.GetInt(mcpPortFlag); ok {
mcpPort := mp
mcpPortPtr = &mcpPort
}
// Optional MCP SQL user
var mcpUserPtr *string
if mu, ok := apr.GetValue(mcpUserFlag); ok {
user := mu
mcpUserPtr = &user
}
// Optional MCP SQL password
var mcpPasswordPtr *string
if mpw, ok := apr.GetValue(mcpPasswordFlag); ok {
pw := mpw
mcpPasswordPtr = &pw
}
// Optional MCP SQL database
var mcpDatabasePtr *string
if mdb, ok := apr.GetValue(mcpDatabaseFlag); ok {
db := mdb
mcpDatabasePtr = &db
}
// Validate and prepare MCP options in dedicated helper
if err := validateAndPrepareMCP(serverConfig, mcpPortPtr, mcpUserPtr, mcpPasswordPtr, mcpDatabasePtr); err != nil {
return err
}
err = generateYamlConfigIfNone(ap, help, args, dEnv, serverConfig)
if err != nil {
return err
@@ -279,12 +320,24 @@ func StartServer(ctx context.Context, versionStr, commandStr string, args []stri
cli.Printf("Starting server with Config %v\n", servercfg.ConfigInfo(serverConfig))
skipRootUserInitialization := apr.Contains(skipRootUserInitialization)
// Build MCP config if any MCP-related options are present
var mcpCfg *MCPConfig
if mcpPortPtr != nil || (mcpUserPtr != nil && *mcpUserPtr != "") || (mcpPasswordPtr != nil && *mcpPasswordPtr != "") || (mcpDatabasePtr != nil && *mcpDatabasePtr != "") {
mcpCfg = &MCPConfig{
Port: mcpPortPtr,
User: mcpUserPtr,
Password: mcpPasswordPtr,
Database: mcpDatabasePtr,
}
}
startError, closeError := Serve(ctx, &Config{
Version: versionStr,
ServerConfig: serverConfig,
Controller: controller,
DoltEnv: dEnv,
SkipRootUserInit: skipRootUserInitialization,
MCP: mcpCfg,
})
if startError != nil {
return startError
+10 -5
View File
@@ -17,7 +17,7 @@ require (
github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.13.0
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/go-sql-driver/mysql v1.9.1
github.com/go-sql-driver/mysql v1.9.3
github.com/gocraft/dbr/v2 v2.7.2
github.com/golang/snappy v0.0.4
github.com/google/uuid v1.6.0
@@ -34,7 +34,7 @@ require (
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.10.0
github.com/tealeg/xlsx v1.0.5
go.uber.org/zap v1.24.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.40.0
golang.org/x/net v0.42.0
golang.org/x/sync v0.16.0
@@ -58,6 +58,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0
github.com/creasty/defaults v1.6.0
github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12
github.com/dolthub/dolt-mcp v0.1.2-0.20250825222220-f6a0cfd9ec3e
github.com/dolthub/eventsapi_schema v0.0.0-20250725194025-a087efa1ee55
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2
github.com/dolthub/go-mysql-server v0.20.1-0.20250825220247-28daf87bfe0e
@@ -157,6 +158,7 @@ require (
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/lestrrat-go/strftime v1.0.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mark3labs/mcp-go v0.34.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/pierrec/lz4/v4 v4.1.6 // indirect
@@ -168,12 +170,14 @@ require (
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/sony/gobreaker v0.5.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.9 // indirect
github.com/tklauser/numcpus v0.3.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
@@ -182,8 +186,7 @@ require (
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.25.0 // indirect
@@ -197,4 +200,6 @@ require (
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)
go 1.24.0
go 1.24.4
toolchain go1.24.6
+16 -10
View File
@@ -165,8 +165,6 @@ github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/bcicen/jstream v1.0.0 h1:gOi+Sn9mHrpePlENynPKA6Dra/PjLaIpqrTevhfvLAA=
github.com/bcicen/jstream v1.0.0/go.mod h1:9ielPxqFry7Y4Tg3j4BfjPocfJ3TbsRtXOAYXYmRuAQ=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -205,6 +203,8 @@ github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waN
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12 h1:IdqX7J8vi/Kn3T3Ee0VzqnLqwFmgA2hr8WZETPcQjfM=
github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12/go.mod h1:rN7X8BHwkjPcfMQQ2QTAq/xM3leUSGLfb+1Js7Y6TVo=
github.com/dolthub/dolt-mcp v0.1.2-0.20250825222220-f6a0cfd9ec3e h1:bqMvf19MdCoaIPthBF3auRxHs2C8V0vs9w1xBUm/25c=
github.com/dolthub/dolt-mcp v0.1.2-0.20250825222220-f6a0cfd9ec3e/go.mod h1:bw/9fg2/fHI/QnSNL24HaL8+K2ybO/Hoy+ETUWwnZ6A=
github.com/dolthub/eventsapi_schema v0.0.0-20250725194025-a087efa1ee55 h1:LdtiPD8IJ+M2XckvfQ/aLG34lET1shz+WMccGBhZRLM=
github.com/dolthub/eventsapi_schema v0.0.0-20250725194025-a087efa1ee55/go.mod h1:CoDLfgPqHyBtth0Cp+fi/CmC4R81zJNX4wPjShdZ+Bw=
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww=
@@ -253,6 +253,8 @@ github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:r
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
@@ -289,8 +291,8 @@ github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
@@ -454,6 +456,8 @@ github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lyft/protoc-gen-star v0.5.2/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU=
github.com/mark3labs/mcp-go v0.34.0 h1:eWy7WBGvhk6EyAAyVzivTCprE52iXJwNtvHV6Cv3bR0=
github.com/mark3labs/mcp-go v0.34.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -566,6 +570,8 @@ github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -613,6 +619,8 @@ github.com/xitongsys/parquet-go-source v0.0.0-20190524061010-2b72cbee77d5/go.mod
github.com/xitongsys/parquet-go-source v0.0.0-20200817004010-026bad9b25d0/go.mod h1:HYhIKsdns7xz80OgkbgJYrtQY7FjHWHKH6cvN7+czGE=
github.com/xitongsys/parquet-go-source v0.0.0-20211010230925-397910c5e371 h1:RfGiOP/lWKBeNgpXmCeandYGV4pAnZsl42kX50p1UgE=
github.com/xitongsys/parquet-go-source v0.0.0-20211010230925-397910c5e371/go.mod h1:qLb2Itmdcp7KPa5KZKvhE9U1q5bYSOmgeOckF/H2rQA=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -659,14 +667,12 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+2 -1
View File
@@ -51,7 +51,8 @@ dolt_log_in_PST() {
}
setup_no_dolt_init() {
export PATH=$PATH:~/go/bin
# Ensure locally installed dolt (via `go install`) takes precedence
export PATH=~/go/bin:$PATH
cd $BATS_TMPDIR
# remove directory if exists
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env bats
load $BATS_TEST_DIRNAME/helper/common.bash
load $BATS_TEST_DIRNAME/helper/query-server-common.bash
make_repo() {
mkdir "$1"
cd "$1"
dolt init
cd ..
}
setup() {
skiponwindows "tests are flaky on Windows"
setup_no_dolt_init
make_repo repo1
}
teardown() {
stop_sql_server 1 && sleep 0.5
rm -rf $BATS_TMPDIR/sql-server-mcp-test$$
teardown_common
}
# Helper: wait for an MCP HTTP server to accept TCP connections on the given port
# wait_for_mcp_port <PORT> <TIMEOUT_MS>
wait_for_mcp_port() {
port=$1
timeout_ms=$2
end_time=$((SECONDS+($timeout_ms/1000)))
while [ $SECONDS -lt $end_time ]; do
nc -z localhost "$port" >/dev/null 2>&1 && return 0
sleep 1
done
return 1
}
@test "sql-server mcp: --mcp-database connects to specified database" {
cd repo1
# Create target database before starting server
dolt sql -q "CREATE DATABASE mcpdb"
MCP_PORT=$( definePORT )
start_sql_server_with_args --host 0.0.0.0 --mcp-port "$MCP_PORT" --mcp-user root --mcp-database mcpdb
run wait_for_mcp_port "$MCP_PORT" 8500
[ $status -eq 0 ]
# initialize and list via MCP
INIT_FILE=$BATS_TMPDIR/mcp_init_md_$$.json
OUT_INIT=$BATS_TMPDIR/mcp_out_init_md_$$.json
echo '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"clientInfo":{"name":"bats","version":"0.0.0"},"capabilities":{}}}' > "$INIT_FILE"
run bash -c "curl -sS -D $BATS_TMPDIR/mcp_headers_md_$$.txt -H 'Content-Type: application/json' --data-binary @'$INIT_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_INIT'"
[ $status -eq 0 ]
SESSION=$(grep -i '^Mcp-Session-Id:' $BATS_TMPDIR/mcp_headers_md_$$.txt | awk -F': ' '{print $2}' | tr -d '\r')
[ -n "$SESSION" ]
CALL_FILE=$BATS_TMPDIR/mcp_call_md_$$.json
OUT_CALL=$BATS_TMPDIR/mcp_out_call_md_$$.json
echo '{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"list_databases","arguments":{}}}' > "$CALL_FILE"
run bash -c "curl -sS -H 'Content-Type: application/json' -H 'Mcp-Session-Id: '$SESSION --data-binary @'$CALL_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_CALL'"
[ $status -eq 0 ]
run grep -E "mcpdb" "$OUT_CALL"
[ $status -eq 0 ]
}
@test "sql-server mcp: --mcp-user authenticates as specified sql user" {
cd repo1
# TODO: this is currently broken in bats for some reason,
# need to figure out why
# Create a sql user with no password and read privileges
# dolt sql -q "CREATE USER mcpuser@'%'"
# dolt sql -q "GRANT ALL PRIVILEGES ON *.* TO mcpuser@'%'"
MCP_PORT=$( definePORT )
start_sql_server_with_args --host 0.0.0.0 --mcp-port "$MCP_PORT" --mcp-user root
run wait_for_mcp_port "$MCP_PORT" 8500
[ $status -eq 0 ]
# initialize and list via MCP
INIT_FILE=$BATS_TMPDIR/mcp_init_mu_$$.json
OUT_INIT=$BATS_TMPDIR/mcp_out_init_mu_$$.json
echo '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"clientInfo":{"name":"bats","version":"0.0.0"},"capabilities":{}}}' > "$INIT_FILE"
run bash -c "curl -sS -D $BATS_TMPDIR/mcp_headers_mu_$$.txt -H 'Content-Type: application/json' --data-binary @'$INIT_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_INIT'"
[ $status -eq 0 ]
SESSION=$(grep -i '^Mcp-Session-Id:' $BATS_TMPDIR/mcp_headers_mu_$$.txt | awk -F': ' '{print $2}' | tr -d '\r')
[ -n "$SESSION" ]
CALL_FILE=$BATS_TMPDIR/mcp_call_mu_$$.json
OUT_CALL=$BATS_TMPDIR/mcp_out_call_mu_$$.json
echo '{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"list_databases","arguments":{}}}' > "$CALL_FILE"
run bash -c "curl -sS -H 'Content-Type: application/json' -H 'Mcp-Session-Id: '$SESSION --data-binary @'$CALL_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_CALL'"
[ $status -eq 0 ]
}
@test "sql-server mcp: starts MCP HTTP server on --mcp-port and serves alongside SQL" {
cd repo1
# Choose distinct ports for SQL and MCP
MCP_PORT=$( definePORT )
# Start the sql-server with MCP enabled
start_sql_server_with_args --host 0.0.0.0 --mcp-port="$MCP_PORT" --mcp-user root
# Verify SQL server accepts connections on $PORT
run dolt sql -q "SELECT 1;"
[ $status -eq 0 ]
# Verify MCP HTTP server port opens
run wait_for_mcp_port "$MCP_PORT" 8500
[ $status -eq 0 ]
# If wget exists, optionally probe a health endpoint (implementation may add this)
if command -v wget >/dev/null 2>&1; then
run wget --quiet --tries=1 --spider "http://127.0.0.1:${MCP_PORT}/health"
# health endpoint may not exist yet; don't assert success here
fi
}
@test "sql-server mcp: without --mcp-port, no MCP port is opened" {
cd repo1
# Pick a free port and ensure server doesn't bind it as MCP when not requested
MCP_PORT=$( definePORT )
# Start the sql-server WITHOUT MCP
start_sql_server
# Verify SQL server accepts connections on $PORT
run dolt sql -q "SELECT 1;"
[ $status -eq 0 ]
# Verify MCP_PORT is not in use (best-effort; we chose an unused port)
run nc -z localhost "$MCP_PORT"
[ $status -ne 0 ]
}
@test "sql-server mcp: HTTP initialize and call list_databases tool" {
cd repo1
if ! command -v curl >/dev/null 2>&1; then
skip "curl not available"
fi
MCP_PORT=$( definePORT )
# Start server with MCP enabled
start_sql_server_with_args --host 0.0.0.0 --mcp-port="$MCP_PORT" --mcp-user root
# Wait for MCP port to accept connections
run wait_for_mcp_port "$MCP_PORT" 8500
[ $status -eq 0 ]
# Initialize session
INIT_FILE=$BATS_TMPDIR/mcp_init_$$.json
OUT_INIT=$BATS_TMPDIR/mcp_out_init_$$.json
cat > "$INIT_FILE" <<'EOF'
{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"clientInfo":{"name":"bats","version":"0.0.0"},"capabilities":{}}}
EOF
run bash -c "curl -sS -D $BATS_TMPDIR/mcp_headers_$$.txt -H 'Content-Type: application/json' --data-binary @'$INIT_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_INIT'"
[ $status -eq 0 ]
# Extract session id from response headers
SESSION=$(grep -i '^Mcp-Session-Id:' $BATS_TMPDIR/mcp_headers_$$.txt | awk -F': ' '{print $2}' | tr -d '\r')
[ -n "$SESSION" ]
# Call list_databases with session header
CALL_FILE=$BATS_TMPDIR/mcp_call_$$.json
OUT_CALL=$BATS_TMPDIR/mcp_out_call_$$.json
cat > "$CALL_FILE" <<'EOF'
{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"list_databases","arguments":{}}}
EOF
run bash -c "curl -sS -H 'Content-Type: application/json' -H 'Mcp-Session-Id: '$SESSION --data-binary @'$CALL_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_CALL'"
[ $status -eq 0 ]
# Expect known databases in response payload (markdown or text content)
run grep -E "information_schema|mysql|repo1" "$OUT_CALL"
[ $status -eq 0 ]
}
@test "sql-server mcp: invalid --mcp-port values (0 and >65535)" {
cd repo1
# mcp-port = 0
SQL_PORT=$( definePORT )
run dolt sql-server --host 0.0.0.0 --port="$SQL_PORT" --socket "dolt.$SQL_PORT.sock" --mcp-port 0 --mcp-user root
[ $status -ne 0 ]
# mcp-port > 65535
SQL_PORT=$( definePORT )
run dolt sql-server --host 0.0.0.0 --port="$SQL_PORT" --socket "dolt.$SQL_PORT.sock" --mcp-port 70000 --mcp-user root
[ $status -ne 0 ]
}
@test "sql-server mcp: --mcp-port identical to --port fails startup" {
cd repo1
PORT=$( definePORT )
run dolt sql-server --host 0.0.0.0 --port="$PORT" --socket "dolt.$PORT.sock" --mcp-port "$PORT" --mcp-user root
[ $status -ne 0 ]
}
@test "sql-server mcp: multiple --mcp-port flags result in error" {
cd repo1
SQL_PORT=$( definePORT )
# Provide duplicate mcp-port flags; expect failure
run dolt sql-server --host 0.0.0.0 --port="$SQL_PORT" --socket "dolt.$SQL_PORT.sock" --mcp-port 45001 --mcp-user root --mcp-port 45002 --mcp-user root
[ $status -ne 0 ]
}
@test "sql-server mcp: any MCP arg without --mcp-port errors" {
cd repo1
SQL_PORT=$( definePORT )
# --mcp-user without --mcp-port should fail
run dolt sql-server --host 0.0.0.0 --port="$SQL_PORT" --socket "dolt.$SQL_PORT.sock" --mcp-user root
[ $status -ne 0 ]
# --mcp-password without --mcp-port should fail
run dolt sql-server --host 0.0.0.0 --port="$SQL_PORT" --socket "dolt.$SQL_PORT.sock" --mcp-password secret
[ $status -ne 0 ]
# --mcp-database without --mcp-port should fail
run dolt sql-server --host 0.0.0.0 --port="$SQL_PORT" --socket "dolt.$SQL_PORT.sock" --mcp-database somedb
[ $status -ne 0 ]
}
@test "sql-server mcp: restart with same --mcp-port succeeds after stop" {
cd repo1
MCP_PORT=$( definePORT )
start_sql_server_with_args --host 0.0.0.0 --mcp-port "$MCP_PORT" --mcp-user root
run wait_for_mcp_port "$MCP_PORT" 8500
[ $status -eq 0 ]
stop_sql_server 1
# Immediate restart on same MCP port
start_sql_server_with_args --host 0.0.0.0 --mcp-port "$MCP_PORT" --mcp-user root
run wait_for_mcp_port "$MCP_PORT" 8500
[ $status -eq 0 ]
# initialize and list
INIT_FILE=$BATS_TMPDIR/mcp_init_rs_$$.json
OUT_INIT=$BATS_TMPDIR/mcp_out_init_rs_$$.json
echo '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"clientInfo":{"name":"bats","version":"0.0.0"},"capabilities":{}}}' > "$INIT_FILE"
run bash -c "curl -sS -D $BATS_TMPDIR/mcp_headers_rs_$$.txt -H 'Content-Type: application/json' --data-binary @'$INIT_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_INIT'"
[ $status -eq 0 ]
SESSION=$(grep -i '^Mcp-Session-Id:' $BATS_TMPDIR/mcp_headers_rs_$$.txt | awk -F': ' '{print $2}' | tr -d '\r')
[ -n "$SESSION" ]
CALL_FILE=$BATS_TMPDIR/mcp_call_rs_$$.json
OUT_CALL=$BATS_TMPDIR/mcp_out_call_rs_$$.json
echo '{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"list_databases","arguments":{}}}' > "$CALL_FILE"
run bash -c "curl -sS -H 'Content-Type: application/json' -H 'Mcp-Session-Id: '$SESSION --data-binary @'$CALL_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_CALL'"
[ $status -eq 0 ]
run grep -E "information_schema|mysql|repo1" "$OUT_CALL"
[ $status -eq 0 ]
}
@test "sql-server mcp: forceful termination closes servers cleanly; restart works" {
cd repo1
MCP_PORT=$( definePORT )
start_sql_server_with_args --host 0.0.0.0 --mcp-port "$MCP_PORT" --mcp-user root
run wait_for_mcp_port "$MCP_PORT" 8500
[ $status -eq 0 ]
# Force kill the server
kill -9 $SERVER_PID
# Give OS a moment to release ports
sleep 1
# Restart with same MCP port
start_sql_server_with_args --host 0.0.0.0 --mcp-port "$MCP_PORT" --mcp-user root
run wait_for_mcp_port "$MCP_PORT" 8500
[ $status -eq 0 ]
# initialize and list
INIT_FILE=$BATS_TMPDIR/mcp_init_fk_$$.json
OUT_INIT=$BATS_TMPDIR/mcp_out_init_fk_$$.json
echo '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"clientInfo":{"name":"bats","version":"0.0.0"},"capabilities":{}}}' > "$INIT_FILE"
run bash -c "curl -sS -D $BATS_TMPDIR/mcp_headers_fk_$$.txt -H 'Content-Type: application/json' --data-binary @'$INIT_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_INIT'"
[ $status -eq 0 ]
SESSION=$(grep -i '^Mcp-Session-Id:' $BATS_TMPDIR/mcp_headers_fk_$$.txt | awk -F': ' '{print $2}' | tr -d '\r')
[ -n "$SESSION" ]
CALL_FILE=$BATS_TMPDIR/mcp_call_fk_$$.json
OUT_CALL=$BATS_TMPDIR/mcp_out_call_fk_$$.json
echo '{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"list_databases","arguments":{}}}' > "$CALL_FILE"
run bash -c "curl -sS -H 'Content-Type: application/json' -H 'Mcp-Session-Id: '$SESSION --data-binary @'$CALL_FILE' http://127.0.0.1:${MCP_PORT}/ > '$OUT_CALL'"
[ $status -eq 0 ]
run grep -E "information_schema|mysql|repo1" "$OUT_CALL"
[ $status -eq 0 ]
}
@@ -1,10 +1,12 @@
module github.com/dolthub/dolt/integration-tests/go-sql-server-driver
go 1.24.0
go 1.24.4
toolchain go1.24.6
require (
github.com/dolthub/dolt/go v0.40.4
github.com/go-sql-driver/mysql v1.9.1
github.com/go-sql-driver/mysql v1.9.3
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.10.0
golang.org/x/sync v0.16.0
@@ -4,8 +4,8 @@ github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BS
github.com/creasty/defaults v1.6.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=