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

1265 lines
44 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 server
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net"
"os"
"runtime/debug"
"slices"
"strings"
"sync/atomic"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
"github.com/dolthub/dolt/go/libraries/doltcore/sqlserver"
"github.com/dolthub/go-mysql-server/server"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/planbuilder"
"github.com/dolthub/go-mysql-server/sql/transform"
"github.com/dolthub/vitess/go/mysql"
"github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/jackc/pgx/v5/pgproto3"
"github.com/jackc/pgx/v5/pgtype"
"github.com/mitchellh/go-ps"
"github.com/sirupsen/logrus"
"github.com/dolthub/doltgresql/core/dataloader"
"github.com/dolthub/doltgresql/postgres/parser/parser"
psql "github.com/dolthub/doltgresql/postgres/parser/parser/sql"
"github.com/dolthub/doltgresql/postgres/parser/sem/tree"
"github.com/dolthub/doltgresql/server/ast"
"github.com/dolthub/doltgresql/server/node"
)
// ConnectionHandler is responsible for the entire lifecycle of a user connection: receiving messages they send,
// executing queries, sending the correct messages in return, and terminating the connection when appropriate.
type ConnectionHandler struct {
mysqlConn *mysql.Conn
preparedStatements map[string]PreparedStatementData
portals map[string]PortalData
doltgresHandler *DoltgresHandler
backend *pgproto3.Backend
waitForSync bool
// copyFromStdinState is set when this connection is in the COPY FROM STDIN mode, meaning it is waiting on
// COPY DATA messages from the client to import data into tables.
copyFromStdinState *copyFromStdinState
// inTransaction is set to true with BEGIN query and false with COMMIT query.
inTransaction bool
}
// Set this env var to disable panic handling in the connection, which is useful when debugging a panic
const disablePanicHandlingEnvVar = "DOLT_PGSQL_PANIC"
// HandlePanics determines whether panics should be handled in the connection handler. See |disablePanicHandlingEnvVar|.
var HandlePanics = true
func init() {
if _, ok := os.LookupEnv(disablePanicHandlingEnvVar); ok {
HandlePanics = false
} else {
// This checks if the Go debugger is attached, so that we can disable panic catching automatically
pid := os.Getppid()
for pid != 0 {
p, err := ps.FindProcess(pid)
if err != nil || p == nil {
break
} else if strings.HasPrefix(p.Executable(), "dlv") {
HandlePanics = false
break
} else {
pid = p.PPid()
}
}
}
}
// NewConnectionHandler returns a new ConnectionHandler for the connection provided
func NewConnectionHandler(conn net.Conn, handler mysql.Handler, sel server.ServerEventListener) *ConnectionHandler {
mysqlConn := &mysql.Conn{
Conn: conn,
PrepareData: make(map[uint32]*mysql.PrepareData),
}
mysqlConn.ConnectionID = atomic.AddUint32(&connectionIDCounter, 1)
// Postgres has a two-stage procedure for prepared queries. First the query is parsed via a |Parse| message, and
// the result is stored in the |preparedStatements| map by the name provided. Then one or more |Bind| messages
// provide parameters for the query, and the result is stored in |portals|. Finally, a call to |Execute| executes
// the named portal.
preparedStatements := make(map[string]PreparedStatementData)
portals := make(map[string]PortalData)
// TODO: possibly should define engine and session manager ourselves
// instead of depending on the GetRunningServer method.
server := sqlserver.GetRunningServer()
doltgresHandler := &DoltgresHandler{
e: server.Engine,
sm: server.SessionManager(),
readTimeout: 0, // cfg.ConnReadTimeout,
encodeLoggedQuery: false, // cfg.EncodeLoggedQuery,
pgTypeMap: pgtype.NewMap(),
}
if sel != nil {
doltgresHandler.sel = sel
}
return &ConnectionHandler{
mysqlConn: mysqlConn,
preparedStatements: preparedStatements,
portals: portals,
doltgresHandler: doltgresHandler,
backend: pgproto3.NewBackend(conn, conn),
}
}
// HandleConnection handles a connection's session, reading messages, executing queries, and sending responses.
// Expected to run in a goroutine per connection.
func (h *ConnectionHandler) HandleConnection() {
var returnErr error
if HandlePanics {
defer func() {
if r := recover(); r != nil {
// debug.Stack() here prints the stack trace of the original panic, not the lexical stack of this defer function
stackTrace := string(debug.Stack())
logrus.Errorf("Listener recovered panic: %v: %s", r, stackTrace)
var eomErr error
if returnErr != nil {
eomErr = returnErr
} else {
eomErr = errors.Errorf("Listener recovered panic: %v: %s", r, stackTrace)
}
// Sending eom can panic, which means we must recover again
defer func() {
if r := recover(); r != nil {
logrus.Errorf("Listener recovered panic: %v: %s", r, string(debug.Stack()))
}
}()
h.endOfMessages(eomErr)
}
if returnErr != nil {
fmt.Println(returnErr.Error())
}
}()
}
defer func() {
if err := h.Conn().Close(); err != nil {
fmt.Printf("Failed to properly close connection:\n%v\n", err)
}
}()
h.doltgresHandler.NewConnection(h.mysqlConn)
defer func() {
h.doltgresHandler.ConnectionClosed(h.mysqlConn)
}()
if proceed, err := h.handleStartup(); err != nil || !proceed {
returnErr = err
return
}
// Main session loop: read messages one at a time off the connection until we receive a |Terminate| message, in
// which case we hang up, or the connection is closed by the client, which generates an io.EOF from the connection.
for {
stop, err := h.receiveMessage()
if err != nil {
returnErr = err
break
}
if stop {
break
}
}
}
// Conn returns the underlying net.Conn for this connection.
func (h *ConnectionHandler) Conn() net.Conn {
return h.mysqlConn.Conn
}
// setConn sets a new underlying net.Conn for this connection.
func (h *ConnectionHandler) setConn(conn net.Conn) {
h.mysqlConn.Conn = conn
h.backend = pgproto3.NewBackend(conn, conn)
}
// handleStartup handles the entire startup routine, including SSL requests, authentication, etc. Returns false if the
// connection has been terminated, or if we should not proceed with the message loop.
func (h *ConnectionHandler) handleStartup() (bool, error) {
startupMessage, err := h.backend.ReceiveStartupMessage()
if err == io.EOF {
// Receiving EOF means that the connection has terminated, so we should just return
return false, nil
} else if err != nil {
return false, errors.Errorf("error receiving startup message: %w", err)
}
switch sm := startupMessage.(type) {
case *pgproto3.StartupMessage:
if err = h.handleAuthentication(sm); err != nil {
return false, err
}
if err = h.sendClientStartupMessages(); err != nil {
return false, err
}
if err = h.chooseInitialParameters(sm); err != nil {
return false, err
}
return true, h.send(&pgproto3.ReadyForQuery{
TxStatus: byte(ReadyForQueryTransactionIndicator_Idle),
})
case *pgproto3.SSLRequest:
hasCertificate := len(certificate.Certificate) > 0
var performSSL = []byte("N")
if hasCertificate {
performSSL = []byte("S")
}
_, err = h.Conn().Write(performSSL)
if err != nil {
return false, errors.Errorf("error sending SSL request: %w", err)
}
// If we have a certificate and the client has asked for SSL support, then we switch here.
// This involves swapping out our underlying net connection for a new one.
// We can't start in SSL mode, as the client does not attempt the handshake until after our response.
if hasCertificate {
h.setConn(tls.Server(h.Conn(), &tls.Config{
Certificates: []tls.Certificate{certificate},
}))
}
return h.handleStartup()
case *pgproto3.GSSEncRequest:
// we don't support GSSAPI
_, err = h.Conn().Write([]byte("N"))
if err != nil {
return false, errors.Errorf("error sending response to GSS Enc Request: %w", err)
}
return h.handleStartup()
default:
return false, errors.Errorf("terminating connection: unexpected start message: %#v", startupMessage)
}
}
// sendClientStartupMessages sends introductory messages to the client and returns any error
func (h *ConnectionHandler) sendClientStartupMessages() error {
if err := h.send(&pgproto3.ParameterStatus{
Name: "server_version",
Value: "15.17",
}); err != nil {
return err
}
if err := h.send(&pgproto3.ParameterStatus{
Name: "client_encoding",
Value: "UTF8",
}); err != nil {
return err
}
if err := h.send(&pgproto3.ParameterStatus{
Name: "standard_conforming_strings",
Value: "on",
}); err != nil {
return err
}
if err := h.send(&pgproto3.ParameterStatus{
Name: "in_hot_standby",
Value: "off",
}); err != nil {
return err
}
return h.send(&pgproto3.BackendKeyData{
ProcessID: processID,
SecretKey: make([]byte, 4), // TODO: this should represent an ID that can uniquely identify this connection, so that CancelRequest will work
})
}
// chooseInitialParameters attempts to choose the initial parameter settings for the connection,
// if one is specified in the startup message provided.
func (h *ConnectionHandler) chooseInitialParameters(startupMessage *pgproto3.StartupMessage) error {
for name, value := range startupMessage.Parameters {
// TODO: handle other parameters defined in StartupMessage
switch strings.ToLower(name) {
case "datestyle":
err := h.doltgresHandler.InitSessionParameterDefault(context.Background(), h.mysqlConn, "DateStyle", value)
if err != nil {
return err
}
}
}
// set initial database
db, ok := startupMessage.Parameters["database"]
dbSpecified := ok && len(db) > 0
if !dbSpecified {
db = h.mysqlConn.User
}
useStmt := fmt.Sprintf("SET database TO '%s';", db)
postgresParser := psql.PostgresParser{}
parsed, err := postgresParser.ParseSimple(useStmt)
if err != nil {
return err
}
err = h.doltgresHandler.ComQuery(context.Background(), h.mysqlConn, useStmt, parsed, func(_ *sql.Context, _ *Result) error {
return nil
})
// If a database isn't specified, then we attempt to connect to a database with the same name as the user,
// ignoring any error
if err != nil && dbSpecified {
_ = h.send(&pgproto3.ErrorResponse{
Severity: string(ErrorResponseSeverity_Fatal),
Code: "3D000",
Message: fmt.Sprintf(`"database "%s" does not exist"`, db),
Routine: "InitPostgres",
})
return err
}
return nil
}
// receiveMessage reads a single message off the connection and processes it, returning an error if no message could be
// received from the connection. Otherwise, (a message is received successfully), the message is processed and any
// error is handled appropriately. The return value indicates whether the connection should be closed.
func (h *ConnectionHandler) receiveMessage() (bool, error) {
var endOfMessages bool
// For the time being, we handle panics in this function and treat them the same as errors so that they don't
// forcibly close the connection. Contrast this with the panic handling logic in HandleConnection, where we treat any
// panic as unrecoverable to the connection. As we fill out the implementation, we can revisit this decision and
// rethink our posture over whether panics should terminate a connection.
if HandlePanics {
defer func() {
if r := recover(); r != nil {
stackTrace := string(debug.Stack())
logrus.Errorf("Listener recovered panic: %v: %s", r, stackTrace)
eomErr := errors.Errorf("receiveMessage recovered panic: %v: %s", r, stackTrace)
if !endOfMessages && h.waitForSync {
if syncErr := h.discardToSync(); syncErr != nil {
fmt.Println(syncErr.Error())
}
}
h.endOfMessages(eomErr)
}
}()
}
msg, err := h.backend.Receive()
if err != nil {
return false, errors.Errorf("error receiving message: %w", err)
}
if m, ok := msg.(json.Marshaler); ok && logrus.IsLevelEnabled(logrus.DebugLevel) {
msgInfo, err := m.MarshalJSON()
if err != nil {
return false, err
}
logrus.Debugf("Received message: %s", string(msgInfo))
} else {
logrus.Debugf("Received message: %t", msg)
}
var stop bool
stop, endOfMessages, err = h.handleMessage(msg)
if err != nil {
if !endOfMessages && h.waitForSync {
if syncErr := h.discardToSync(); syncErr != nil {
fmt.Println(syncErr.Error())
}
}
h.endOfMessages(err)
} else if endOfMessages {
h.endOfMessages(nil)
}
return stop, nil
}
// handleMessages processes the message provided and returns status flags indicating what the connection should do next.
// If the |stop| response parameter is true, it indicates that the connection should be closed by the caller. If the
// |endOfMessages| response parameter is true, it indicates that no more messages are expected for the current operation
// and a READY FOR QUERY message should be sent back to the client, so it can send the next query.
func (h *ConnectionHandler) handleMessage(msg pgproto3.Message) (stop, endOfMessages bool, err error) {
switch message := msg.(type) {
case *pgproto3.Terminate:
return true, false, nil
case *pgproto3.Sync:
h.waitForSync = false
return false, true, nil
case *pgproto3.Query:
endOfMessages, err = h.handleQuery(message)
return false, endOfMessages, err
case *pgproto3.Parse:
return false, false, h.handleParse(message)
case *pgproto3.Describe:
return false, false, h.handleDescribe(message)
case *pgproto3.Bind:
return false, false, h.handleBind(message)
case *pgproto3.Execute:
return false, false, h.handleExecute(message)
case *pgproto3.Close:
if message.ObjectType == 'S' {
delete(h.preparedStatements, message.Name)
} else {
delete(h.portals, message.Name)
}
return false, false, h.send(&pgproto3.CloseComplete{})
case *pgproto3.CopyData:
return h.handleCopyData(message)
case *pgproto3.CopyDone:
return h.handleCopyDone(message)
case *pgproto3.CopyFail:
return h.handleCopyFail(message)
default:
return false, true, errors.Errorf(`unhandled message "%t"`, message)
}
}
// handleQuery handles a query message, and returns a boolean flag, |endOfMessages| indicating if no other messages are
// expected as part of this query, in which case the server will send a READY FOR QUERY message back to the client so
// that it can send its next query.
func (h *ConnectionHandler) handleQuery(message *pgproto3.Query) (endOfMessages bool, err error) {
handled, err := h.handledPSQLCommands(message.String)
if handled || err != nil {
return true, err
}
queries, err := h.convertQuery(message.String)
if err != nil {
if printErrorStackTraces {
fmt.Printf("Error parsing query: %+v\n", err)
}
return true, err
}
// A query message destroys the unnamed statement and the unnamed portal
delete(h.preparedStatements, "")
delete(h.portals, "")
if len(queries) == 1 {
// empty query special case
if queries[0].AST == nil {
return true, h.send(&pgproto3.EmptyQueryResponse{})
}
handled, endOfMessages, err = h.handleQueryOutsideEngine(queries[0])
if handled {
return endOfMessages, err
}
return true, h.query(queries[0])
}
for _, query := range queries {
handled, _, err = h.handleQueryOutsideEngine(query)
if err != nil {
return true, err
}
if handled {
continue
}
err = h.query(query)
if err != nil {
return true, err
}
}
return true, nil
}
// handleQueryOutsideEngine handles any queries that should be handled by the handler directly, rather than being
// passed to the engine. The response parameter |handled| is true if the query was handled, |endOfMessages| is true
// if no more messages are expected for this query and server should send the client a READY FOR QUERY message,
// and any error that occurred while handling the query.
func (h *ConnectionHandler) handleQueryOutsideEngine(query ConvertedQuery) (handled bool, endOfMessages bool, err error) {
switch stmt := query.AST.(type) {
case *sqlparser.Begin:
h.inTransaction = true
case *sqlparser.Commit:
h.inTransaction = false
case *sqlparser.Deallocate:
return true, true, h.deallocatePreparedStatement(stmt.Name, h.preparedStatements, query, h.Conn())
case sqlparser.InjectedStatement:
switch injectedStmt := stmt.Statement.(type) {
case node.DiscardStatement:
return true, true, h.discardAll(query)
case *node.CopyFrom:
// When copying data from STDIN, the data is sent to the server as CopyData messages
// We send endOfMessages=false since the server will be in COPY DATA mode and won't
// be ready for more queries util COPY DATA mode is completed.
if injectedStmt.Stdin {
return true, false, h.handleCopyFromStdinQuery(injectedStmt, h.Conn())
} else {
// copying from a file is handled in a single message
return true, true, h.copyFromFileQuery(injectedStmt)
}
}
}
return false, true, nil
}
// handleParse handles a parse message, returning any error that occurs
func (h *ConnectionHandler) handleParse(message *pgproto3.Parse) error {
h.waitForSync = true
// TODO: "Named prepared statements must be explicitly closed before they can be redefined by another Parse message, but this is not required for the unnamed statement"
queries, err := h.convertQuery(message.Query)
if err != nil {
if printErrorStackTraces {
fmt.Printf("Error parsing query: %+v\n", err)
}
return err
}
if len(queries) != 1 {
return errors.Errorf("cannot insert multiple commands into a prepared statement")
}
query := queries[0]
if query.AST == nil {
// special case: empty query
h.preparedStatements[message.Name] = PreparedStatementData{
Query: query,
}
return nil
}
ctx, err := h.doltgresHandler.sm.NewContextWithQuery(context.Background(), h.mysqlConn, query.String)
if err != nil {
return err
}
parsedQuery, fields, err := h.doltgresHandler.ComPrepareParsed(ctx, h.mysqlConn, query.String, query.AST)
if err != nil {
return err
}
analyzedPlan, ok := parsedQuery.(sql.Node)
if !ok {
return errors.Errorf("expected a sql.Node, got %T", parsedQuery)
}
// A valid Parse message must have ParameterObjectIDs if there are any binding variables.
bindVarTypes := message.ParameterOIDs
// Clients can specify an OID of zero to indicate that the type should be inferred. If we
// see any zero OIDs, we fall back to extracting the bind var types from the plan.
if len(bindVarTypes) == 0 || slices.Contains(bindVarTypes, 0) {
// NOTE: This is used for Prepared Statement Tests only.
bindVarTypes, err = extractBindVarTypes(ctx, analyzedPlan)
if err != nil {
return err
}
}
h.preparedStatements[message.Name] = PreparedStatementData{
Query: query,
ReturnFields: fields,
BindVarTypes: bindVarTypes,
}
return h.send(&pgproto3.ParseComplete{})
}
// handleDescribe handles a Describe message, returning any error that occurs
func (h *ConnectionHandler) handleDescribe(message *pgproto3.Describe) error {
var fields []pgproto3.FieldDescription
var bindvarTypes []uint32
var query ConvertedQuery
h.waitForSync = true
if message.ObjectType == 'S' {
preparedStatementData, ok := h.preparedStatements[message.Name]
if !ok {
return errors.Errorf("prepared statement %s does not exist", message.Name)
}
fields = preparedStatementData.ReturnFields
bindvarTypes = preparedStatementData.BindVarTypes
query = preparedStatementData.Query
} else {
portalData, ok := h.portals[message.Name]
if !ok {
return errors.Errorf("portal %s does not exist", message.Name)
}
fields = portalData.Fields
query = portalData.Query
}
return h.sendDescribeResponse(fields, bindvarTypes, query)
}
// handleBind handles a bind message, returning any error that occurs
func (h *ConnectionHandler) handleBind(message *pgproto3.Bind) error {
h.waitForSync = true
// TODO: a named portal object lasts till the end of the current transaction, unless explicitly destroyed
// we need to destroy the named portal as a side effect of the transaction ending
logrus.Tracef("binding portal %q to prepared statement %s", message.DestinationPortal, message.PreparedStatement)
preparedData, ok := h.preparedStatements[message.PreparedStatement]
if !ok {
return errors.Errorf("prepared statement %s does not exist", message.PreparedStatement)
}
if preparedData.Query.AST == nil {
// special case: empty query
h.portals[message.DestinationPortal] = PortalData{
Query: preparedData.Query,
IsEmptyQuery: true,
}
return h.send(&pgproto3.BindComplete{})
}
analyzedPlan, fields, err := h.doltgresHandler.ComBind(
context.Background(),
h.mysqlConn,
preparedData.Query.String,
preparedData.Query.AST,
BindVariables{
varTypes: preparedData.BindVarTypes,
formatCodes: message.ParameterFormatCodes,
parameters: message.Parameters,
},
message.ResultFormatCodes)
if err != nil {
return err
}
boundPlan, ok := analyzedPlan.(sql.Node)
if !ok {
return errors.Errorf("expected a sql.Node, got %T", analyzedPlan)
}
resultFormatCodes, err := extendFormatCodes(len(fields), message.ResultFormatCodes)
if err != nil {
return err
}
h.portals[message.DestinationPortal] = PortalData{
Query: preparedData.Query,
Fields: fields,
BoundPlan: boundPlan,
FormatCodes: resultFormatCodes,
}
return h.send(&pgproto3.BindComplete{})
}
// handleExecute handles an execute message, returning any error that occurs
func (h *ConnectionHandler) handleExecute(message *pgproto3.Execute) error {
h.waitForSync = true
// TODO: implement the RowMax
portalData, ok := h.portals[message.Portal]
if !ok {
return errors.Errorf("portal %s does not exist", message.Portal)
}
logrus.Tracef("executing portal %s with contents %v", message.Portal, portalData)
query := portalData.Query
if portalData.IsEmptyQuery {
return h.send(&pgproto3.EmptyQueryResponse{})
}
// Certain statement types get handled directly by the handler instead of being passed to the engine
handled, _, err := h.handleQueryOutsideEngine(query)
if handled {
return err
}
// |rowsAffected| gets altered by the callback below
rowsAffected := int32(0)
callback := h.spoolRowsCallback(query, &rowsAffected, true)
err = h.doltgresHandler.ComExecuteBound(context.Background(), h.mysqlConn, query.String, portalData.BoundPlan, portalData.FormatCodes, callback)
if err != nil {
return err
}
return h.send(makeCommandComplete(query.StatementTag, rowsAffected))
}
func makeCommandComplete(tag string, rows int32) *pgproto3.CommandComplete {
switch tag {
case "INSERT", "DELETE", "UPDATE", "MERGE", "SELECT", "CREATE TABLE AS", "MOVE", "FETCH", "COPY":
if tag == "INSERT" {
tag = "INSERT 0"
}
tag = fmt.Sprintf("%s %d", tag, rows)
}
return &pgproto3.CommandComplete{
CommandTag: []byte(tag),
}
}
// handleCopyData handles the COPY DATA message, by loading the data sent from the client. The |stop| response parameter
// is true if the connection handler should shut down the connection, |endOfMessages| is true if no more COPY DATA
// messages are expected, and the server should tell the client that it is ready for the next query, and |err| contains
// any error that occurred while processing the COPY DATA message.
func (h *ConnectionHandler) handleCopyData(message *pgproto3.CopyData) (stop bool, endOfMessages bool, err error) {
copyFromData := bytes.NewReader(message.Data)
stop, endOfMessages, err = h.handleCopyDataHelper(h.copyFromStdinState, copyFromData)
if err != nil && h.copyFromStdinState != nil {
h.copyFromStdinState.copyErr = err
}
return stop, endOfMessages, err
}
// copyFromFileQuery handles a COPY FROM message that is reading from a file, returning any error that occurs
func (h *ConnectionHandler) copyFromFileQuery(stmt *node.CopyFrom) error {
copyState := &copyFromStdinState{
copyFromStdinNode: stmt,
}
// TODO: security check for file path
// TODO: Privilege Checking: https://www.postgresql.org/docs/15/sql-copy.html
f, err := os.Open(stmt.File)
if err != nil {
return err
}
defer f.Close()
_, _, err = h.handleCopyDataHelper(copyState, f)
if err != nil {
return err
}
sqlCtx, err := h.doltgresHandler.NewContext(context.Background(), h.mysqlConn, "")
if err != nil {
return err
}
loadDataResults, err := copyState.dataLoader.Finish(sqlCtx)
if err != nil {
return err
}
if sqlCtx.GetTransaction() != nil && sqlCtx.GetIgnoreAutoCommit() {
txSession, ok := sqlCtx.Session.(sql.TransactionSession)
if !ok {
return errors.Errorf("session does not implement sql.TransactionSession")
}
if err = txSession.CommitTransaction(sqlCtx, txSession.GetTransaction()); err != nil {
return err
}
sqlCtx.SetIgnoreAutoCommit(false)
}
return h.send(&pgproto3.CommandComplete{
CommandTag: []byte(fmt.Sprintf("COPY %d", loadDataResults.RowsLoaded)),
})
}
// handleCopyDataHelper is a helper function that should only be invoked by handleCopyData. handleCopyData wraps this
// function so that it can capture any returned error message and store it in the saved state.
func (h *ConnectionHandler) handleCopyDataHelper(copyState *copyFromStdinState, copyFromData io.Reader) (stop bool, endOfMessages bool, err error) {
if copyState == nil {
return false, true, errors.Errorf("COPY DATA message received without a COPY FROM STDIN operation in progress")
}
// Grab a sql.Context and ensure the session has a transaction started, otherwise the copied data
// won't get committed correctly.
sqlCtx, err := h.doltgresHandler.NewContext(context.Background(), h.mysqlConn, "COPY FROM STDIN")
if err != nil {
return false, false, err
}
if err = startTransactionIfNecessary(sqlCtx); err != nil {
return false, false, err
}
if copyState.copyFromStdinNode.TableName.Schema != "" {
originalSchema, err := sqlCtx.GetSessionVariable(sqlCtx, "search_path")
if err != nil {
return false, false, err
}
err = sqlCtx.SetSessionVariable(sqlCtx, "search_path", copyState.copyFromStdinNode.TableName.Schema)
if err != nil {
return false, false, err
}
defer func() {
_ = sqlCtx.SetSessionVariable(sqlCtx, "search_path", originalSchema)
}()
}
dataLoader := copyState.dataLoader
if dataLoader == nil {
copyFromStdinNode := copyState.copyFromStdinNode
if copyFromStdinNode == nil {
return false, false, errors.Errorf("no COPY FROM STDIN node found")
}
// we build an insert node to use for the full insert plan, for which the copy from node will be the row source
builder := planbuilder.New(sqlCtx, h.doltgresHandler.e.Analyzer.Catalog, nil)
node, flags, err := builder.BindOnly(copyFromStdinNode.InsertStub, "", nil)
if err != nil {
return false, false, err
}
insertNode, ok := node.(*plan.InsertInto)
if !ok {
return false, false, errors.Errorf("expected plan.InsertInto, got %T", node)
}
// now that we have our insert node, we can build the data loader
tbl := getInsertableTable(insertNode.Destination)
if tbl == nil {
// this should be impossible, enforced by analyzer above
return false, false, errors.Errorf("no insertable table found in %v", insertNode.Destination)
}
switch copyFromStdinNode.CopyOptions.CopyFormat {
case tree.CopyFormatText:
dataLoader, err = dataloader.NewTabularDataLoader(insertNode.ColumnNames, tbl.Schema(sqlCtx), copyFromStdinNode.CopyOptions.Delimiter, "", copyFromStdinNode.CopyOptions.Header)
case tree.CopyFormatCsv:
dataLoader, err = dataloader.NewCsvDataLoader(insertNode.ColumnNames, tbl.Schema(sqlCtx), copyFromStdinNode.CopyOptions.Delimiter, copyFromStdinNode.CopyOptions.Header)
case tree.CopyFormatBinary:
err = errors.Errorf("BINARY format is not supported for COPY FROM")
default:
err = errors.Errorf("unknown format specified for COPY FROM: %v",
copyFromStdinNode.CopyOptions.CopyFormat)
}
if err != nil {
return false, false, err
}
// we have to set the data loader on the copyFrom node before we analyze it, because we need the loader's
// schema to analyze
copyState.copyFromStdinNode.DataLoader = dataLoader
// After building out stub insert node, swap out the source node with the COPY node, then analyze the entire thing
node = insertNode.WithSource(copyFromStdinNode)
analyzedNode, err := h.doltgresHandler.e.Analyzer.Analyze(sqlCtx, node, nil, flags)
if err != nil {
return false, false, err
}
copyState.insertNode = analyzedNode
copyState.dataLoader = dataLoader
}
reader := bufio.NewReader(copyFromData)
if err = dataLoader.SetNextDataChunk(sqlCtx, reader); err != nil {
return false, false, err
}
callback := func(_ *sql.Context, _ *Result) error { return nil }
err = h.doltgresHandler.ComExecuteBound(sqlCtx, h.mysqlConn, "COPY FROM", copyState.insertNode, nil, callback)
if err != nil {
return false, false, err
}
// We expect to see more CopyData messages until we see either a CopyDone or CopyFail message, so
// return false for endOfMessages
return false, false, nil
}
// Returns the first sql.InsertableTable node found in the tree provided, or nil if none is found.
func getInsertableTable(node sql.Node) sql.InsertableTable {
var tbl sql.InsertableTable
transform.Inspect(node, func(node sql.Node) bool {
if rt, ok := node.(*plan.ResolvedTable); ok {
if insertable, ok := rt.Table.(sql.InsertableTable); ok {
tbl = insertable
return false
}
}
return true
})
return tbl
}
// handleCopyDone handles a COPY DONE message by finalizing the in-progress COPY DATA operation and committing the
// loaded table data. The |stop| response parameter is true if the connection handler should shut down the connection,
// |endOfMessages| is true if no more COPY DATA messages are expected, and the server should tell the client that it is
// ready for the next query, and |err| contains any error that occurred while processing the COPY DATA message.
func (h *ConnectionHandler) handleCopyDone(_ *pgproto3.CopyDone) (stop bool, endOfMessages bool, err error) {
if h.copyFromStdinState == nil {
return false, true,
errors.Errorf("COPY DONE message received without a COPY FROM STDIN operation in progress")
}
// If there was a previous error returned from processing a CopyData message, then don't return an error here
// and don't send endOfMessage=true, since the CopyData error already sent endOfMessage=true. If we do send
// endOfMessage=true here, then the client gets confused about the unexpected/extra Idle message since the
// server has already reported it was idle in the last message after the returned error.
if h.copyFromStdinState.copyErr != nil {
return false, false, nil
}
dataLoader := h.copyFromStdinState.dataLoader
if dataLoader == nil {
return false, true,
errors.Errorf("no data loader found for COPY FROM STDIN operation")
}
sqlCtx, err := h.doltgresHandler.NewContext(context.Background(), h.mysqlConn, "")
if err != nil {
return false, false, err
}
loadDataResults, err := dataLoader.Finish(sqlCtx)
if err != nil {
return false, false, err
}
// TODO: rather than always committing the transaction here, we should respect whether a transaction was
// expliclitly started and not commit if not. In order to do that, we need to not always set
// ctx.GetIgnoreAutoCommit(), and instead conditionally *not* insert a transaction closing iterator during chunk
// processing. We need a new query flag to effectively do the latter though.
txSession, ok := sqlCtx.Session.(sql.TransactionSession)
if !ok {
return false, false, errors.Errorf("session does not implement sql.TransactionSession")
}
if err = txSession.CommitTransaction(sqlCtx, txSession.GetTransaction()); err != nil {
return false, false, err
}
sqlCtx.SetIgnoreAutoCommit(false)
h.copyFromStdinState = nil
// We send back endOfMessage=true, since the COPY DONE message ends the COPY DATA flow and the server is ready
// to accept the next query now.
return false, true, h.send(&pgproto3.CommandComplete{
CommandTag: []byte(fmt.Sprintf("COPY %d", loadDataResults.RowsLoaded)),
})
}
// handleCopyFail handles a COPY FAIL message by aborting the in-progress COPY DATA operation. The |stop| response
// parameter is true if the connection handler should shut down the connection, |endOfMessages| is true if no more
// COPY DATA messages are expected, and the server should tell the client that it is ready for the next query, and
// |err| contains any error that occurred while processing the COPY DATA message.
func (h *ConnectionHandler) handleCopyFail(_ *pgproto3.CopyFail) (stop bool, endOfMessages bool, err error) {
if h.copyFromStdinState == nil {
return false, true,
errors.Errorf("COPY FAIL message received without a COPY FROM STDIN operation in progress")
}
dataLoader := h.copyFromStdinState.dataLoader
if dataLoader == nil {
return false, true,
errors.Errorf("no data loader found for COPY FROM STDIN operation")
}
h.copyFromStdinState = nil
// We send back endOfMessage=true, since the COPY FAIL message ends the COPY DATA flow and the server is ready
// to accept the next query now.
return false, true, nil
}
// startTransactionIfNecessary checks to see if the current session has a transaction started yet or not, and if not,
// creates a read/write transaction for the session to use. This is necessary for handling commands that alter
// data without going through the GMS engine.
func startTransactionIfNecessary(ctx *sql.Context) error {
doltSession, ok := ctx.Session.(*dsess.DoltSession)
if !ok {
return errors.Errorf("unexpected session type: %T", ctx.Session)
}
if doltSession.GetTransaction() == nil {
if _, err := doltSession.StartTransaction(ctx, sql.ReadWrite); err != nil {
return err
}
// When we start a transaction ourselves, we must ignore auto-commit settings for transaction
ctx.SetIgnoreAutoCommit(true)
}
return nil
}
// deallocatePreparedStatement handles a DEALLOCATE statement by deleting the corresponding prepared statement from the
// handler's prepared statement map, and sending a CommandComplete message back to the client. Pass an empty |name|
// for `ALL`. This matches the behavior in the parser, which doesn't include a separate field for ALL.
func (h *ConnectionHandler) deallocatePreparedStatement(name string, preparedStatements map[string]PreparedStatementData, query ConvertedQuery, conn net.Conn) error {
if name == "" {
for name := range preparedStatements {
delete(preparedStatements, name)
}
} else {
_, ok := preparedStatements[name]
if !ok {
return errors.Errorf("prepared statement %s does not exist", name)
}
delete(preparedStatements, name)
}
return h.send(&pgproto3.CommandComplete{
CommandTag: []byte(query.StatementTag),
})
}
// query runs the given query and sends a CommandComplete message to the client
func (h *ConnectionHandler) query(query ConvertedQuery) error {
// |rowsAffected| gets altered by the callback below
rowsAffected := int32(0)
callback := h.spoolRowsCallback(query, &rowsAffected, false)
err := h.doltgresHandler.ComQuery(context.Background(), h.mysqlConn, query.String, query.AST, callback)
if err != nil {
if strings.HasPrefix(err.Error(), "syntax error at position") {
return errors.Errorf("This statement is not yet supported")
}
return err
}
return h.send(makeCommandComplete(query.StatementTag, rowsAffected))
}
// spoolRowsCallback returns a callback function that will send RowDescription message,
// then a DataRow message for each row in the result set.
func (h *ConnectionHandler) spoolRowsCallback(query ConvertedQuery, rows *int32, isExecute bool) func(ctx *sql.Context, res *Result) error {
// IsIUD returns whether the query is either an INSERT, UPDATE, or DELETE query.
isIUD := query.StatementTag == "INSERT" || query.StatementTag == "UPDATE" || query.StatementTag == "DELETE"
// The RowDescription message should only be sent once, before any DataRow messages,
// otherwise some clients will not properly handle results.
hasSentRowDescription := false
return func(ctx *sql.Context, res *Result) error {
sess := dsess.DSessFromSess(ctx.Session)
for _, notice := range sess.Notices() {
backendMsg, ok := notice.(pgproto3.BackendMessage)
if !ok {
return fmt.Errorf("unexpected notice message type: %T", notice)
}
if err := h.send(backendMsg); err != nil {
return err
}
}
sess.ClearNotices()
if returnsRow(query) {
// EXECUTE does not send RowDescription; instead it should be sent from DESCRIBE prior to it
if !isExecute && !hasSentRowDescription {
hasSentRowDescription = true
h.backend.Send(&pgproto3.RowDescription{
Fields: res.Fields,
})
}
// res.Rows should be length rowsBatch = 128
for _, row := range res.Rows {
h.backend.Send(&pgproto3.DataRow{
Values: row.val,
})
}
err := h.backend.Flush()
if err != nil {
return err
}
}
if isIUD {
*rows = int32(res.RowsAffected)
} else {
*rows += int32(len(res.Rows))
}
return nil
}
}
// sendDescribeResponse sends a response message for a Describe message
func (h *ConnectionHandler) sendDescribeResponse(fields []pgproto3.FieldDescription, types []uint32, query ConvertedQuery) error {
// The prepared statement variant of the describe command returns the OIDs of the parameters.
if types != nil {
if err := h.send(&pgproto3.ParameterDescription{
ParameterOIDs: types,
}); err != nil {
return err
}
}
if returnsRow(query) {
// Both variants finish with a row description.
return h.send(&pgproto3.RowDescription{
Fields: fields,
})
} else {
return h.send(&pgproto3.NoData{})
}
}
// handledPSQLCommands handles the special PSQL commands, such as \l and \dt.
func (h *ConnectionHandler) handledPSQLCommands(statement string) (bool, error) {
statement = strings.ToLower(statement)
// Command: \d table_name
if strings.HasPrefix(statement, "select c.oid,\n n.nspname,\n c.relname\nfrom pg_catalog.pg_class c\n left join pg_catalog.pg_namespace n on n.oid = c.relnamespace\nwhere c.relname operator(pg_catalog.~) '^(") && strings.HasSuffix(statement, ")$' collate pg_catalog.default\n and pg_catalog.pg_table_is_visible(c.oid)\norder by 2, 3;") {
// There are >at least< 15 separate statements sent for this command, which is far too much to validate and
// implement, so we'll just return an error for now
return true, errors.Errorf("PSQL command not yet supported")
}
// Command: \df
if statement == "select n.nspname as \"schema\",\n p.proname as \"name\",\n pg_catalog.pg_get_function_result(p.oid) as \"result data type\",\n pg_catalog.pg_get_function_arguments(p.oid) as \"argument data types\",\n case p.prokind\n when 'a' then 'agg'\n when 'w' then 'window'\n when 'p' then 'proc'\n else 'func'\n end as \"type\"\nfrom pg_catalog.pg_proc p\n left join pg_catalog.pg_namespace n on n.oid = p.pronamespace\nwhere pg_catalog.pg_function_is_visible(p.oid)\n and n.nspname <> 'pg_catalog'\n and n.nspname <> 'information_schema'\norder by 1, 2, 4;" {
return true, h.query(ConvertedQuery{
String: `SELECT '' AS "Schema", '' AS "Name", '' AS "Result data type", '' AS "Argument data types", '' AS "Type" LIMIT 0;`,
StatementTag: "SELECT",
})
}
return false, nil
}
// endOfMessages should be called from HandleConnection or a function within HandleConnection. This represents the end
// of the message slice, which may occur naturally (all relevant response messages have been sent) or on error. Once
// endOfMessages has been called, no further messages should be sent, and the connection loop should wait for the next
// query. A nil error should be provided if this is being called naturally.
func (h *ConnectionHandler) endOfMessages(err error) {
if err != nil {
// TODO: is ReadyForQueryTransactionIndicator_FailedTransactionBlock used here?
h.sendError(err)
}
ti := ReadyForQueryTransactionIndicator_Idle
if h.inTransaction {
ti = ReadyForQueryTransactionIndicator_TransactionBlock
}
if sendErr := h.send(&pgproto3.ReadyForQuery{
TxStatus: byte(ti),
}); sendErr != nil {
// We panic here for the same reason as above.
panic(sendErr)
}
}
// sendError sends the given error to the client. This should generally never be called directly.
func (h *ConnectionHandler) sendError(err error) {
fmt.Println(err.Error())
if sendErr := h.send(&pgproto3.ErrorResponse{
Severity: string(ErrorResponseSeverity_Error),
Code: "XX000", // internal_error for now
Message: err.Error(),
}); sendErr != nil {
// If we're unable to send anything to the connection, then there's something wrong with the connection and
// we should terminate it. This will be caught in HandleConnection's defer block.
panic(sendErr)
}
}
// convertQuery takes the given Postgres query, and converts it as an ast.ConvertedQuery that will work with the handler.
// If the query string contains multiple queries, then multiple ConvertedQuery will be returned.
func (h *ConnectionHandler) convertQuery(query string) ([]ConvertedQuery, error) {
s, err := parser.Parse(query)
if err != nil {
return nil, err
}
if len(s) == 0 {
return []ConvertedQuery{{String: query}}, nil
}
converted := make([]ConvertedQuery, len(s))
for i := range s {
vitessAST, err := ast.Convert(s[i])
stmtTag := s[i].AST.StatementTag()
if err != nil {
return nil, err
}
if vitessAST == nil {
converted[i] = ConvertedQuery{
String: s[i].AST.String(),
StatementTag: stmtTag,
}
} else {
converted[i] = ConvertedQuery{
String: query,
AST: vitessAST,
StatementTag: stmtTag,
}
}
}
return converted, nil
}
// discardAll handles the DISCARD ALL command
func (h *ConnectionHandler) discardAll(query ConvertedQuery) error {
err := h.doltgresHandler.ComResetConnection(h.mysqlConn)
if err != nil {
return err
}
return h.send(&pgproto3.CommandComplete{
CommandTag: []byte(query.StatementTag),
})
}
// handleCopyFromStdinQuery handles the COPY FROM STDIN query at the Doltgres layer, without passing it to the engine.
// COPY FROM STDIN can't be handled directly by the GMS engine, since COPY FROM STDIN relies on multiple messages sent
// over the wire.
func (h *ConnectionHandler) handleCopyFromStdinQuery(copyFrom *node.CopyFrom, conn net.Conn) error {
h.copyFromStdinState = &copyFromStdinState{
copyFromStdinNode: copyFrom,
}
return h.send(&pgproto3.CopyInResponse{
OverallFormat: 0,
})
}
// DiscardToSync discards all messages in the buffer until a Sync has been reached. If a Sync was never sent, then this
// may cause the connection to lock until the client send a Sync, as their request structure was malformed.
func (h *ConnectionHandler) discardToSync() error {
for {
message, err := h.backend.Receive()
if err != nil {
return err
}
if _, ok := message.(*pgproto3.Sync); ok {
return nil
}
}
}
// Send sends the given message over the connection.
func (h *ConnectionHandler) send(message pgproto3.BackendMessage) error {
h.backend.Send(message)
return h.backend.Flush()
}
// returnsRow returns whether the query returns set of rows such as SELECT and FETCH statements.
func returnsRow(query ConvertedQuery) bool {
switch query.StatementTag {
case "SELECT", "SHOW", "FETCH", "EXPLAIN", "SHOW TABLES", "SHOW CREATE", "SHOW INDEXES FROM TABLE", "SHOW DATABASES", "SHOW SCHEMAS":
return true
case "INSERT", "UPDATE", "DELETE":
return hasReturningClause(query.AST)
default:
return false
}
}
// hasReturningClause return true if |statement| has a RETURNING clause defined.
func hasReturningClause(statement sqlparser.Statement) bool {
hasReturningClause := false
sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) {
switch node := node.(type) {
case *sqlparser.Update:
if len(node.Returning) > 0 {
hasReturningClause = true
}
return false, nil
case *sqlparser.Insert:
if len(node.Returning) > 0 {
hasReturningClause = true
}
return false, nil
case *sqlparser.Delete:
if len(node.Returning) > 0 {
hasReturningClause = true
}
}
return true, nil
}, statement)
return hasReturningClause
}