Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

263 lines
7.4 KiB
Go

// Copyright 2023 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"
"crypto/tls"
sql2 "database/sql"
"fmt"
"io"
"net"
"regexp"
"strings"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/go-sql-driver/mysql"
"github.com/gocraft/dbr/v2"
"github.com/gocraft/dbr/v2/dialect"
"github.com/dolthub/dolt/go/cmd/dolt/cli"
"github.com/dolthub/dolt/go/cmd/dolt/commands/engine"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/servercfg"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlutil"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
"github.com/dolthub/dolt/go/libraries/utils/filesys"
)
type QueryistTLSMode int
const (
QueryistTLSMode_Disabled QueryistTLSMode = iota
// Require TLS, verify the server certificate using the system
// trust store, do not allow fallback to plaintext.
//
// Used for `dolt --host ... sql ...` when `--no-tls-` is not
// specified. Often used for connecting to Hosted DoltDB
// instances using the CLI commands posted on
// hosted.doltdb.com.
QueryistTLSMode_Enabled
// Used for local Dolt CLI queryist connecting to the running
// local server. In this mode, TLS is allowed but not required
// and the client does not verify the remote TLS
// certificate. It is assumed connecting to the port locally
// is secure and lands the client in the correct place, given
// the contents of sql-server.info, for example.
//
// This mode still does not allow the Dolt CLI to connect to a
// server which requires a client certificate.
QueryistTLSMode_NoVerify_FallbackToPlaintext
)
// BuildConnectionStringQueryist returns a [cli.LateBindQueryist] that opens a connection to the
// server at |host|:|port| using |creds| and |tlsMode|, and selects |dbRev| as the default
// database. |configName| and |configEmail| are used as the commit identity when the client
// connects over a loopback address; non-loopback connections read the identity from
// CURRENT_USER() so the grant host matches whatever the server assigned.
func BuildConnectionStringQueryist(_ context.Context, cwdFS filesys.Filesys, creds *cli.UserPassword, apr *argparser.ArgParseResults, host string, port int, tlsMode QueryistTLSMode, dbRev string, configName, configEmail string) (cli.LateBindQueryist, error) {
clientConfig, err := GetClientConfig(cwdFS, creds, apr)
if err != nil {
return nil, err
}
// ParseDSN currently doesn't support `/` in the db name
dbName, _ := doltdb.SplitRevisionDbName(dbRev)
parsedMySQLConfig, err := mysql.ParseDSN(servercfg.ConnectionString(clientConfig, dbName))
if err != nil {
return nil, err
}
parsedMySQLConfig.DBName = dbRev
parsedMySQLConfig.Addr = fmt.Sprintf("%s:%d", host, port)
switch tlsMode {
case QueryistTLSMode_Disabled:
case QueryistTLSMode_Enabled:
parsedMySQLConfig.TLS = &tls.Config{}
case QueryistTLSMode_NoVerify_FallbackToPlaintext:
parsedMySQLConfig.TLS = &tls.Config{InsecureSkipVerify: true}
parsedMySQLConfig.AllowFallbackToPlaintext = true
}
mysqlConnector, err := mysql.NewConnector(parsedMySQLConfig)
if err != nil {
return nil, err
}
conn := &dbr.Connection{DB: sql2.OpenDB(mysqlConnector), EventReceiver: nil, Dialect: dialect.MySQL}
gatherWarnings := false
queryist := ConnectionQueryist{connection: conn, gatherWarnings: &gatherWarnings}
var lateBind cli.LateBindQueryist = func(ctx context.Context, opts ...cli.LateBindQueryistOption) (res cli.LateBindQueryistResult, err error) {
sqlCtx := sql.NewContext(ctx)
sqlCtx.SetCurrentDatabase(dbRev)
ip := net.ParseIP(host)
if host == "localhost" || (ip != nil && ip.IsLoopback()) {
if err := engine.InitClientCommitIdentSession(queryist, sqlCtx, configName, configEmail); err != nil {
cli.PrintErr(err.Error())
}
}
res.Queryist = queryist
res.Context = sqlCtx
res.Closer = func() {
conn.Close()
}
res.IsRemote = true
return res, nil
}
return lateBind, nil
}
// ConnectionQueryist executes queries by connecting to a running mySql server.
type ConnectionQueryist struct {
connection *dbr.Connection
gatherWarnings *bool
}
var _ cli.Queryist = &ConnectionQueryist{}
func (c ConnectionQueryist) EnableGatherWarnings() {
*c.gatherWarnings = true
}
func (c ConnectionQueryist) Query(ctx *sql.Context, query string) (sql.Schema, sql.RowIter, *sql.QueryFlags, error) {
rows, err := c.connection.QueryContext(ctx, query)
if err != nil {
return nil, nil, nil, err
}
rowIter, err := NewMysqlRowWrapper(rows)
if err != nil {
return nil, nil, nil, err
}
if c.gatherWarnings != nil && *c.gatherWarnings == true {
ctx.ClearWarnings()
re := regexp.MustCompile(`\s+`)
noSpace := strings.TrimSpace(re.ReplaceAllString(query, " "))
isShowWarnings := strings.EqualFold(noSpace, "show warnings")
if !isShowWarnings {
warnRows, err := c.connection.QueryContext(ctx, "show warnings")
if err != nil {
return nil, nil, nil, err
}
for warnRows.Next() {
var code int
var msg string
var level string
err = warnRows.Scan(&level, &code, &msg)
if err != nil {
return nil, nil, nil, err
}
ctx.Warn(code, "%s", msg)
}
}
}
return rowIter.Schema(ctx), rowIter, nil, nil
}
func (c ConnectionQueryist) QueryWithBindings(ctx *sql.Context, query string, _ sqlparser.Statement, _ map[string]sqlparser.Expr, _ *sql.QueryFlags) (sql.Schema, sql.RowIter, *sql.QueryFlags, error) {
return c.Query(ctx, query)
}
type MysqlRowWrapper struct {
rows []sql.Row
schema sql.Schema
numRows int
curRow int
}
var _ sql.RowIter = (*MysqlRowWrapper)(nil)
func NewMysqlRowWrapper(sqlRows *sql2.Rows) (*MysqlRowWrapper, error) {
colTypes, err := sqlRows.ColumnTypes()
if err != nil {
return nil, err
}
schema := make(sql.Schema, len(colTypes))
vRow := make([]*string, len(colTypes))
iRow := make([]interface{}, len(colTypes))
rows := make([]sql.Row, 0)
for i, colType := range colTypes {
schema[i] = &sql.Column{
Name: colType.Name(),
Type: sqlutil.DatabaseTypeNameToSqlType(colType.DatabaseTypeName()),
Nullable: true,
}
iRow[i] = &vRow[i]
}
for sqlRows.Next() {
err := sqlRows.Scan(iRow...)
if err != nil {
return nil, err
}
sqlRow := make(sql.Row, len(vRow))
for i, val := range vRow {
if val != nil {
sqlRow[i] = *val
}
}
rows = append(rows, sqlRow)
}
closeErr := sqlRows.Close()
if closeErr != nil {
return nil, err
}
return &MysqlRowWrapper{
rows: rows,
schema: schema,
numRows: len(rows),
curRow: 0,
}, nil
}
func (s *MysqlRowWrapper) Schema(ctx *sql.Context) sql.Schema {
return s.schema
}
func (s *MysqlRowWrapper) Next(*sql.Context) (sql.Row, error) {
if s.NoMoreRows() {
return nil, io.EOF
}
s.curRow++
return s.rows[s.curRow-1], nil
}
func (s *MysqlRowWrapper) NoMoreRows() bool {
return s.curRow >= s.numRows
}
func (s *MysqlRowWrapper) Close(*sql.Context) error {
s.curRow = s.numRows
return nil
}