// 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 := ©FromStdinState{ 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 = ©FromStdinState{ 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 }