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

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
}
}
}()
}