263 lines
7.4 KiB
Go
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
|
|
}
|