281 lines
8.5 KiB
Go
281 lines
8.5 KiB
Go
// Copyright 2025 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package dumps
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/dolthub/go-mysql-server/server"
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
"github.com/jackc/pgx/v5/pgproto3"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// ImportQueryError contains both a query and its associated error.
|
|
type ImportQueryError struct {
|
|
Query string
|
|
Error string
|
|
}
|
|
|
|
// InterceptArgs are the arguments that are passed to InterceptImportMessages.
|
|
type InterceptArgs struct {
|
|
DoltgresPort int
|
|
SkippedQueries []string
|
|
BreakpointQueries []string
|
|
TriggerBreakpoint func(string)
|
|
}
|
|
|
|
// passthroughArguments are the arguments that are passed to createPassthrough.
|
|
type passthroughArguments struct {
|
|
qeChan chan ImportQueryError
|
|
terminate *sync.WaitGroup
|
|
psqlConnBackend *pgproto3.Backend
|
|
doltgresConnFrontend *pgproto3.Frontend
|
|
triggerBreakpoint func(string)
|
|
skippedQueries []string
|
|
breakpointQueries []string
|
|
}
|
|
|
|
// InterceptImportMessages sits between PSQL and Doltgres, returning all error messages that are encountered. As we rely
|
|
// on PSQL to handle the import process, we normally wouldn't be able to associate error messages with queries, as this
|
|
// information is not returned by PSQL itself. Therefore, we create our own connection to Doltgres, and a server that
|
|
// PSQL listens to. We then forward everything from PSQL to Doltgres, while inspecting the messages as they come and go.
|
|
func InterceptImportMessages(t *testing.T, args InterceptArgs) (int, chan ImportQueryError) {
|
|
psqlPort, err := sql.GetEmptyPort()
|
|
require.NoError(t, err)
|
|
qeChan := make(chan ImportQueryError)
|
|
listener, err := server.NewListener("tcp", fmt.Sprintf("127.0.0.1:%d", psqlPort), "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return psqlPort, qeChan
|
|
}
|
|
timer := time.NewTimer(5 * time.Second)
|
|
timer.Stop()
|
|
go func() {
|
|
<-timer.C
|
|
_ = listener.Close()
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
psqlConn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
timer.Stop()
|
|
terminate := &sync.WaitGroup{}
|
|
terminate.Add(1)
|
|
psqlConnBackend := pgproto3.NewBackend(psqlConn, psqlConn)
|
|
doltgresConn, err := (&net.Dialer{}).Dial("tcp", fmt.Sprintf("127.0.0.1:%d", args.DoltgresPort))
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
doltgresConnFrontend := pgproto3.NewFrontend(doltgresConn, doltgresConn)
|
|
|
|
if err = handleStartup(t, psqlConnBackend, doltgresConnFrontend, psqlConn); err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
createPassthrough(passthroughArguments{
|
|
qeChan: qeChan,
|
|
terminate: terminate,
|
|
psqlConnBackend: psqlConnBackend,
|
|
doltgresConnFrontend: doltgresConnFrontend,
|
|
triggerBreakpoint: args.TriggerBreakpoint,
|
|
skippedQueries: args.SkippedQueries,
|
|
breakpointQueries: args.BreakpointQueries,
|
|
})
|
|
terminate.Wait()
|
|
_ = psqlConn.Close()
|
|
_ = doltgresConn.Close()
|
|
timer.Reset(5 * time.Second)
|
|
}
|
|
}()
|
|
return psqlPort, qeChan
|
|
}
|
|
|
|
// handleStartup handles the startup messages.
|
|
func handleStartup(t *testing.T, psqlConnBackend *pgproto3.Backend, doltgresConnFrontend *pgproto3.Frontend, clientConn net.Conn) error {
|
|
StartupLoop:
|
|
for {
|
|
startupMessage, err := psqlConnBackend.ReceiveStartupMessage()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch startupMessage := startupMessage.(type) {
|
|
case *pgproto3.SSLRequest:
|
|
if _, err = clientConn.Write([]byte{'N'}); err != nil {
|
|
return err
|
|
}
|
|
case *pgproto3.StartupMessage:
|
|
doltgresConnFrontend.Send(startupMessage)
|
|
if err = doltgresConnFrontend.Flush(); err != nil {
|
|
return err
|
|
}
|
|
response, err := doltgresConnFrontend.Receive()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = setAuthType(psqlConnBackend, response); err != nil {
|
|
return err
|
|
}
|
|
psqlConnBackend.Send(response)
|
|
if err = psqlConnBackend.Flush(); err != nil {
|
|
return err
|
|
}
|
|
break StartupLoop
|
|
case *pgproto3.GSSEncRequest:
|
|
// we don't support GSSAPI
|
|
_, err = clientConn.Write([]byte("N"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected startup message: %v", startupMessage)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setAuthType sets the client's authentication type depending on the message received from the server. This is
|
|
// necessary, as the client needs the proper context to know how to parse the returned messages.
|
|
func setAuthType(clientConnBackend *pgproto3.Backend, message pgproto3.BackendMessage) error {
|
|
switch message.(type) {
|
|
case *pgproto3.AuthenticationOk:
|
|
return clientConnBackend.SetAuthType(pgproto3.AuthTypeOk)
|
|
case *pgproto3.AuthenticationCleartextPassword:
|
|
return clientConnBackend.SetAuthType(pgproto3.AuthTypeCleartextPassword)
|
|
case *pgproto3.AuthenticationMD5Password:
|
|
return clientConnBackend.SetAuthType(pgproto3.AuthTypeMD5Password)
|
|
case *pgproto3.AuthenticationGSS:
|
|
return clientConnBackend.SetAuthType(pgproto3.AuthTypeGSS)
|
|
case *pgproto3.AuthenticationGSSContinue:
|
|
return clientConnBackend.SetAuthType(pgproto3.AuthTypeGSSCont)
|
|
case *pgproto3.AuthenticationSASL:
|
|
return clientConnBackend.SetAuthType(pgproto3.AuthTypeSASL)
|
|
case *pgproto3.AuthenticationSASLContinue:
|
|
return clientConnBackend.SetAuthType(pgproto3.AuthTypeSASLContinue)
|
|
case *pgproto3.AuthenticationSASLFinal:
|
|
return clientConnBackend.SetAuthType(pgproto3.AuthTypeSASLFinal)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// createPassthrough creates the go routines that will read from and write to the connections.
|
|
func createPassthrough(args passthroughArguments) {
|
|
lastQuery := ""
|
|
writeMutex := &sync.Mutex{}
|
|
go func() {
|
|
defer args.terminate.Done()
|
|
for {
|
|
psqlMessage, err := args.psqlConnBackend.Receive()
|
|
if err != nil {
|
|
errStr := err.Error()
|
|
if errStr != "unexpected EOF" && !strings.HasSuffix(errStr, "use of closed network connection") {
|
|
fmt.Println(err)
|
|
}
|
|
return
|
|
}
|
|
switch msg := psqlMessage.(type) {
|
|
case *pgproto3.Query:
|
|
writeMutex.Lock()
|
|
if len(lastQuery) == 0 {
|
|
lastQuery = msg.String
|
|
}
|
|
writeMutex.Unlock()
|
|
for _, query := range args.skippedQueries {
|
|
if strings.HasPrefix(msg.String, query) {
|
|
// An empty query allows for the proper response messages to be sent.
|
|
msg.String = ";"
|
|
break
|
|
}
|
|
}
|
|
for _, query := range args.breakpointQueries {
|
|
if strings.HasPrefix(msg.String, query) {
|
|
args.triggerBreakpoint(msg.String)
|
|
break
|
|
}
|
|
}
|
|
case *pgproto3.Terminate:
|
|
return
|
|
}
|
|
args.doltgresConnFrontend.Send(psqlMessage)
|
|
if err = args.doltgresConnFrontend.Flush(); err != nil {
|
|
errStr := err.Error()
|
|
if errStr != "unexpected EOF" && !strings.HasSuffix(errStr, "use of closed network connection") {
|
|
fmt.Println(err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
go func() {
|
|
for {
|
|
doltgresMessage, err := args.doltgresConnFrontend.Receive()
|
|
if err != nil {
|
|
errStr := err.Error()
|
|
if errStr != "unexpected EOF" &&
|
|
!strings.HasSuffix(errStr, "use of closed network connection") &&
|
|
!strings.HasSuffix(errStr, "An existing connection was forcibly closed by the remote host.") {
|
|
fmt.Println(err)
|
|
}
|
|
return
|
|
}
|
|
switch msg := doltgresMessage.(type) {
|
|
case *pgproto3.ErrorResponse:
|
|
writeMutex.Lock()
|
|
if len(lastQuery) == 0 {
|
|
args.qeChan <- ImportQueryError{
|
|
Query: "UNKNOWN QUERY HAS ERRORED",
|
|
Error: msg.Message,
|
|
}
|
|
} else {
|
|
args.qeChan <- ImportQueryError{
|
|
Query: lastQuery,
|
|
Error: msg.Message,
|
|
}
|
|
}
|
|
writeMutex.Unlock()
|
|
case *pgproto3.ReadyForQuery:
|
|
writeMutex.Lock()
|
|
lastQuery = ""
|
|
writeMutex.Unlock()
|
|
default:
|
|
if err = setAuthType(args.psqlConnBackend, doltgresMessage); err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
}
|
|
args.psqlConnBackend.Send(doltgresMessage)
|
|
if err = args.psqlConnBackend.Flush(); err != nil {
|
|
errStr := err.Error()
|
|
if errStr != "unexpected EOF" &&
|
|
!strings.HasSuffix(errStr, "use of closed network connection") &&
|
|
!strings.HasSuffix(errStr, "An existing connection was forcibly closed by the remote host.") {
|
|
fmt.Println(err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|