chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2024 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 main
import "github.com/jackc/pgx/v5/pgproto3"
// 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
}
}
+300
View File
@@ -0,0 +1,300 @@
// Copyright 2024 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 main
import (
"database/sql/driver"
"encoding/hex"
"encoding/json"
"fmt"
"math"
"strings"
"time"
"github.com/cockroachdb/errors"
"github.com/dolthub/go-mysql-server/sql"
"github.com/jackc/pgx/v5/pgproto3"
"github.com/jackc/pgx/v5/pgtype"
"github.com/dolthub/doltgresql/utils"
)
// CompareRowsOrdered compares the two rows, enforcing that the order matches between the two rows.
func CompareRowsOrdered(aRowDesc, bRowDesc *pgproto3.RowDescription, aRows, bRows []*pgproto3.DataRow) error {
if len(aRows) != len(bRows) {
return errors.Errorf("expected a row count of %d but received %d", len(aRows), len(bRows))
}
aReadRows := ReadRows(aRowDesc, aRows)
bReadRows := ReadRows(bRowDesc, bRows)
for rowIdx := range aReadRows {
if len(aReadRows[rowIdx]) != len(bReadRows[rowIdx]) {
return errors.Errorf("expected a row column count of %d but received %d",
len(aReadRows[rowIdx]), len(bReadRows[rowIdx]))
}
for colIdx := range aReadRows[rowIdx] {
if aReadRows[rowIdx][colIdx] != bReadRows[rowIdx][colIdx] {
if len(aReadRows)+len(bReadRows) < 8 {
return errors.Errorf("row sets differ:\n%s", rowsToErrorString(aReadRows, bReadRows))
} else {
return errors.Errorf("rows differ\n Postgres:\n {%s}\n Doltgres:\n {%s}",
RowToString(aReadRows[rowIdx]), RowToString(bReadRows[rowIdx]))
}
}
}
}
return nil
}
// CompareRowsUnordered compares the two rows. Order is not enforced, however if there are any duplicate rows, then it
// is expected that the duplicate counts match.
func CompareRowsUnordered(aRowDesc, bRowDesc *pgproto3.RowDescription, aRows, bRows []*pgproto3.DataRow) error {
if len(aRows) != len(bRows) {
return errors.Errorf("expected a row count of %d but received %d", len(aRows), len(bRows))
}
// It's possible that two different rows can hash to the same result, but we're not concerned with that.
// The same row will always output the same hash, and that's the only property that we really care about.
aMap := make(map[string]int)
bMap := make(map[string]int)
aReadRows := ReadRows(aRowDesc, aRows)
bReadRows := ReadRows(bRowDesc, bRows)
for rowIdx := range aReadRows {
// Column counts should always match, so this is a sanity check
if len(aReadRows[rowIdx]) != len(bReadRows[rowIdx]) {
return errors.Errorf("expected a row column count of %d but received %d",
len(aReadRows[rowIdx]), len(bReadRows[rowIdx]))
}
// We'll use the string form of each row as though it were a hash
aHash := RowToString(aReadRows[rowIdx])
bHash := RowToString(bReadRows[rowIdx])
if count, ok := aMap[aHash]; ok {
aMap[aHash] = count + 1
} else {
aMap[aHash] = 1
}
if count, ok := bMap[bHash]; ok {
bMap[bHash] = count + 1
} else {
bMap[bHash] = 1
}
}
aKVs := utils.GetMapKVsSorted(aMap)
bKVs := utils.GetMapKVsSorted(bMap)
// One map may have duplicates that the other does not have, so we have to do a length check again
if len(aKVs) != len(bKVs) {
aCount := 0
bCount := 0
for _, aKV := range aKVs {
aCount += aKV.Value
}
for _, bKV := range bKVs {
bCount += bKV.Value
}
if aCount+bCount < 8 {
return errors.Errorf("row sets differ:\n%s", rowKVsToErrorString(aKVs, bKVs))
} else {
return errors.New("row sets differ (too large to display)")
}
}
for i := range aKVs {
if aKVs[i].Key != bKVs[i].Key || aKVs[i].Value != bKVs[i].Value {
aCount := 0
bCount := 0
for _, aKV := range aKVs {
aCount += aKV.Value
}
for _, bKV := range bKVs {
bCount += bKV.Value
}
if aCount+bCount < 8 {
return errors.Errorf("row sets differ:\n%s", rowKVsToErrorString(aKVs, bKVs))
} else {
if aKVs[i].Key != bKVs[i].Key {
return errors.Errorf("could not find the following row in the result set:\n {%s}", aKVs[i].Key)
} else {
return errors.Errorf("for the following row, expected to find %d duplicates but found %d:\n {%s}",
aKVs[i].Value, bKVs[i].Value, aKVs[i].Key)
}
}
}
}
return nil
}
// ReadRows reads the given rows into their native Go types.
func ReadRows(rowDesc *pgproto3.RowDescription, rows []*pgproto3.DataRow) []sql.Row {
if len(rows) == 0 {
return nil
}
typeMap := pgtype.NewMap()
results := make([]sql.Row, len(rows))
for rowIdx, row := range rows {
resultRow := make(sql.Row, len(rowDesc.Fields))
// This should never happen, but we can't be too safe since it's technically possible
if len(rowDesc.Fields) != len(row.Values) {
continue
}
for colIdx := range rowDesc.Fields {
field := rowDesc.Fields[colIdx]
if err := typeMap.Scan(field.DataTypeOID, field.Format, row.Values[colIdx], &resultRow[colIdx]); err != nil {
resultRow[colIdx] = string(row.Values[colIdx])
}
switch val := resultRow[colIdx].(type) {
case bool:
case int:
resultRow[colIdx] = int64(val)
case int8:
case int16:
case int32:
case int64:
case uint:
resultRow[colIdx] = uint64(val)
case uint8:
case uint16:
case uint32:
case uint64:
case float32:
case float64:
case string:
case []byte:
resultRow[colIdx] = hex.EncodeToString(val)
case [16]byte:
resultRow[colIdx] = hex.EncodeToString(val[:])
case time.Time:
case pgtype.InfinityModifier:
switch val {
case pgtype.Infinity:
resultRow[colIdx] = math.Inf(1)
case pgtype.Finite:
resultRow[colIdx] = float64(0)
case pgtype.NegativeInfinity:
resultRow[colIdx] = math.Inf(-1)
}
case pgtype.Range[interface{}]:
resultRow[colIdx] = string(row.Values[colIdx])
case pgtype.Multirange[pgtype.Range[interface{}]]:
resultRow[colIdx] = string(row.Values[colIdx])
case map[string]any:
resultRow[colIdx], _ = json.Marshal(val)
case []any:
resultRow[colIdx] = "[" + RowToString(val) + "]"
case nil:
default:
if driverValue, ok := val.(driver.Valuer); ok {
resultRow[colIdx], _ = driverValue.Value()
} else if stringer, ok := val.(fmt.Stringer); ok {
resultRow[colIdx] = stringer.String()
} else {
// This makes it much simpler for the sake of comparison, but may be wrong.
// At the time of writing (meaning no further updates to the test files), all types are covered.
resultRow[colIdx] = nil
}
}
}
results[rowIdx] = resultRow
}
return results
}
// RowToString returns the row as a string, which may be used for printing.
func RowToString(row sql.Row) string {
values := make([]string, len(row))
for i := range row {
switch val := row[i].(type) {
case bool:
if val {
values[i] = "true"
} else {
values[i] = "false"
}
case int:
values[i] = fmt.Sprintf("%d", val)
case int8:
values[i] = fmt.Sprintf("%d", val)
case int16:
values[i] = fmt.Sprintf("%d", val)
case int32:
values[i] = fmt.Sprintf("%d", val)
case int64:
values[i] = fmt.Sprintf("%d", val)
case uint:
values[i] = fmt.Sprintf("%d", val)
case uint8:
values[i] = fmt.Sprintf("%d", val)
case uint16:
values[i] = fmt.Sprintf("%d", val)
case uint32:
values[i] = fmt.Sprintf("%d", val)
case uint64:
values[i] = fmt.Sprintf("%d", val)
case float32:
values[i] = fmt.Sprintf("%f", val)
case float64:
values[i] = fmt.Sprintf("%f", val)
case string:
values[i] = fmt.Sprintf(`"%s"`, strings.ReplaceAll(val, `"`, `\"`))
case time.Time:
values[i] = val.String()
case nil:
values[i] = "\uFFFD"
default:
values[i] = fmt.Sprintf("%v", val)
}
}
return strings.Join(values, ", ")
}
// rowsToErrorString returns the given rows formatted to be displayed in the regression comment.
func rowsToErrorString(postgresRows []sql.Row, doltgresRows []sql.Row) string {
sb := strings.Builder{}
sb.WriteString(" Postgres:\n")
for _, row := range postgresRows {
sb.WriteString(" {")
sb.WriteString(RowToString(row))
sb.WriteString("}\n")
}
sb.WriteString(" Doltgres:\n")
for _, row := range doltgresRows {
sb.WriteString(" {")
sb.WriteString(RowToString(row))
sb.WriteString("}\n")
}
returnErr := sb.String()
// Removing the last newline since it's not necessary
return returnErr[:len(returnErr)-1]
}
// rowKVsToErrorString returns the given row KVs formatted to be displayed in the regression comment.
func rowKVsToErrorString(postgresRows []utils.KeyValue[string, int], doltgresRows []utils.KeyValue[string, int]) string {
sb := strings.Builder{}
sb.WriteString(" Postgres:\n")
for _, kv := range postgresRows {
for i := 0; i < kv.Value; i++ {
sb.WriteString(" {")
sb.WriteString(kv.Key)
sb.WriteString("}\n")
}
}
sb.WriteString(" Doltgres:\n")
for _, kv := range doltgresRows {
for i := 0; i < kv.Value; i++ {
sb.WriteString(" {")
sb.WriteString(kv.Key)
sb.WriteString("}\n")
}
}
returnErr := sb.String()
// Removing the last newline since it's not necessary
return returnErr[:len(returnErr)-1]
}
+135
View File
@@ -0,0 +1,135 @@
// Copyright 2024 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 main
import (
"errors"
"net"
"time"
"github.com/jackc/pgx/v5/pgproto3"
)
// Connection contains the connection that is used by the replay system for interacting with the Doltgres server.
type Connection struct {
frontend *pgproto3.Frontend
connection net.Conn
timeout time.Duration
reader *MessageReader
startup *pgproto3.StartupMessage
bmChan chan pgproto3.BackendMessage
errChan chan error
}
// NewConnection returns a new Connection.
func NewConnection(network string, reader *MessageReader, timeout time.Duration) (*Connection, error) {
connection, err := (&net.Dialer{}).Dial("tcp", network)
if err != nil {
return nil, err
}
return &Connection{
frontend: pgproto3.NewFrontend(connection, connection),
connection: connection,
timeout: timeout,
reader: reader,
startup: nil,
bmChan: make(chan pgproto3.BackendMessage),
errChan: make(chan error),
}, nil
}
// Close closes the internal connection.
func (c *Connection) Close() {
_ = c.connection.Close()
}
// EmptyReceiveBuffer empties the buffer used by Receive. Returns an error if the buffer was not empty.
func (c *Connection) EmptyReceiveBuffer() error {
if c.frontend.ReadBufferLen() > 0 {
for c.frontend.ReadBufferLen() > 0 {
_, _ = c.frontend.Receive()
}
return errors.New("Doltgres sent additional messages after ReadyForQuery")
}
return nil
}
// Queue adds the given messages to the queue, which will be sent when Send is called. This is generally useful for when
// messages may conditionally be added, and we want to send all messages at once rather than one at a time.
func (c *Connection) Queue(messages ...pgproto3.FrontendMessage) {
for _, message := range messages {
if startupMessage, ok := message.(*pgproto3.StartupMessage); ok {
c.startup = startupMessage
}
c.frontend.Send(message)
}
}
// Receive returns the next message from the backend. If an error is returned, then the connection has been closed, and
// a new one should be opened.
func (c *Connection) Receive() (pgproto3.BackendMessage, error) {
var message pgproto3.BackendMessage
go func() {
var err error
message, err = c.frontend.Receive()
c.errChan <- err
}()
return message, c.handleErrorChannel(false, false)
}
// Send sends the given messages (or messages added via Queue) over the wire. Will sync the reader to the next query on
// error. If such behavior is not desired, then use SendNoSync. If an error is returned, then the connection has been
// closed, and a new one should be opened.
func (c *Connection) Send(messages ...pgproto3.FrontendMessage) error {
c.Queue(messages...)
go func() {
c.errChan <- c.frontend.Flush()
}()
return c.handleErrorChannel(true, true)
}
// SendNoSync is the same as Send, except that the reader will NOT sync to the next query if an error is encountered.
// This should only be called when we can guarantee that the reader is already synchronized to the next query. If an
// error is returned, then the connection has been closed, and a new one should be opened.
func (c *Connection) SendNoSync(messages ...pgproto3.FrontendMessage) error {
c.Queue(messages...)
go func() {
c.errChan <- c.frontend.Flush()
}()
return c.handleErrorChannel(false, true)
}
// handleErrorChannel handles whether the Connection should close once an item has been sent to the error channel. If an
// error is returned, then the Connection has been closed.
func (c *Connection) handleErrorChannel(syncOnError bool, isSend bool) error {
var err error
select {
case err = <-c.errChan:
case <-time.After(c.timeout):
if isSend {
err = errors.New("timeout during Send")
} else {
err = errors.New("timeout during Receive")
}
}
if err != nil {
if syncOnError {
c.reader.SyncToNextQuery()
}
c.reader.PushQueue(c.startup, &pgproto3.ReadyForQuery{TxStatus: 'I'})
_ = c.connection.Close()
}
return err
}
@@ -0,0 +1,75 @@
// Copyright 2024 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 main
import (
"context"
"fmt"
"time"
"github.com/dolthub/dolt/go/libraries/utils/svcs"
"github.com/dolthub/go-mysql-server/sql"
"github.com/jackc/pgx/v5"
dserver "github.com/dolthub/doltgresql/server"
"github.com/dolthub/doltgresql/servercfg"
"github.com/dolthub/doltgresql/servercfg/cfgdetails"
)
// CreateDoltgresServer creates and returns a Doltgres server.
func CreateDoltgresServer() (controller *svcs.Controller, port int, err error) {
port, err = sql.GetEmptyPort()
if err != nil {
return nil, 0, err
}
address := "127.0.0.1"
logLevel := cfgdetails.LogLevel_Panic
controller, err = dserver.RunInMemory(&servercfg.DoltgresConfig{
DoltgresConfig: cfgdetails.DoltgresConfig{
LogLevelStr: &logLevel,
ListenerConfig: &cfgdetails.DoltgresListenerConfig{
PortNumber: &port,
HostStr: &address,
},
},
}, dserver.NewListener)
if err != nil {
return nil, 0, err
}
defer func() {
if err != nil {
controller.Stop()
}
}()
return controller, port, func() error {
// The connection attempt may be made before the server has grabbed the port, so we'll retry the first
// connection a few times.
var conn *pgx.Conn
var err error
ctx := context.Background()
for i := 0; i < 3; i++ {
conn, err = pgx.Connect(ctx, fmt.Sprintf("postgres://postgres:password@127.0.0.1:%d/", port))
if err == nil {
break
} else {
time.Sleep(time.Second)
}
}
if err != nil {
return err
}
return conn.Close(ctx)
}()
}
+136
View File
@@ -0,0 +1,136 @@
// Copyright 2024 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 main
import (
"github.com/jackc/pgx/v5/pgproto3"
)
// FilterMessages removes all "unnecessary" messages so that only the important messages remain.
func FilterMessages(messages []pgproto3.Message) []pgproto3.Message {
filteredMessage := make([]pgproto3.Message, 0, len(messages))
for _, message := range messages {
switch message.(type) {
case *pgproto3.AuthenticationCleartextPassword:
case *pgproto3.AuthenticationGSS:
case *pgproto3.AuthenticationGSSContinue:
case *pgproto3.AuthenticationMD5Password:
case *pgproto3.AuthenticationOk:
case *pgproto3.AuthenticationSASL:
case *pgproto3.AuthenticationSASLContinue:
case *pgproto3.AuthenticationSASLFinal:
case *pgproto3.BackendKeyData:
case *pgproto3.Bind:
filteredMessage = append(filteredMessage, message)
case *pgproto3.BindComplete:
filteredMessage = append(filteredMessage, message)
case *pgproto3.CancelRequest:
filteredMessage = append(filteredMessage, message)
case *pgproto3.Close:
filteredMessage = append(filteredMessage, message)
case *pgproto3.CloseComplete:
filteredMessage = append(filteredMessage, message)
case *pgproto3.CommandComplete:
filteredMessage = append(filteredMessage, message)
case *pgproto3.CopyBothResponse:
filteredMessage = append(filteredMessage, message)
case *pgproto3.CopyData:
filteredMessage = append(filteredMessage, message)
case *pgproto3.CopyDone:
filteredMessage = append(filteredMessage, message)
case *pgproto3.CopyFail:
filteredMessage = append(filteredMessage, message)
case *pgproto3.CopyInResponse:
filteredMessage = append(filteredMessage, message)
case *pgproto3.CopyOutResponse:
filteredMessage = append(filteredMessage, message)
case *pgproto3.DataRow:
filteredMessage = append(filteredMessage, message)
case *pgproto3.Describe:
filteredMessage = append(filteredMessage, message)
case *pgproto3.EmptyQueryResponse:
filteredMessage = append(filteredMessage, message)
case *pgproto3.ErrorResponse:
filteredMessage = append(filteredMessage, message)
case *pgproto3.Execute:
filteredMessage = append(filteredMessage, message)
case *pgproto3.Flush:
case *pgproto3.FunctionCall:
filteredMessage = append(filteredMessage, message)
case *pgproto3.FunctionCallResponse:
filteredMessage = append(filteredMessage, message)
case *pgproto3.GSSEncRequest:
case *pgproto3.GSSResponse:
case *pgproto3.NoData:
filteredMessage = append(filteredMessage, message)
case *pgproto3.NoticeResponse:
case *pgproto3.NotificationResponse:
case *pgproto3.ParameterDescription:
case *pgproto3.ParameterStatus:
case *pgproto3.Parse:
filteredMessage = append(filteredMessage, message)
case *pgproto3.ParseComplete:
filteredMessage = append(filteredMessage, message)
case *pgproto3.PasswordMessage:
case *pgproto3.PortalSuspended:
case *pgproto3.Query:
filteredMessage = append(filteredMessage, message)
case *pgproto3.ReadyForQuery:
filteredMessage = append(filteredMessage, message)
case *pgproto3.RowDescription:
filteredMessage = append(filteredMessage, message)
case *pgproto3.SASLInitialResponse:
case *pgproto3.SASLResponse:
case *pgproto3.SSLRequest:
case *pgproto3.StartupMessage:
filteredMessage = append(filteredMessage, message)
case *pgproto3.Sync:
filteredMessage = append(filteredMessage, message)
case *pgproto3.Terminate:
filteredMessage = append(filteredMessage, message)
default:
// We'll ignore any messages that we don't know about
}
}
return filteredMessage
}
// SplitByTerminate splits the given set of messages by the Terminate message, which represents a test boundary.
func SplitByTerminate(messages []pgproto3.Message) [][]pgproto3.Message {
var messageGroups [][]pgproto3.Message
start := 0
for i, message := range messages {
if _, ok := message.(*pgproto3.Terminate); ok {
messageGroups = append(messageGroups, messages[start:i+1])
start = i + 1
}
}
if start < len(messages)-1 {
messageGroups = append(messageGroups, messages[start:])
}
return messageGroups
}
// CombineGroups combines message groups into a single group.
func CombineGroups(messageGroups ...[]pgproto3.Message) []pgproto3.Message {
if len(messageGroups) == 0 {
return nil
}
newSlice := append([]pgproto3.Message{}, messageGroups[0]...)
for i := 1; i < len(messageGroups); i++ {
newSlice = append(newSlice, messageGroups[i]...)
}
return newSlice
}
+235
View File
@@ -0,0 +1,235 @@
// Copyright 2024 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 main
import (
"fmt"
"net"
"os"
"strings"
"sync"
"time"
"github.com/dolthub/go-mysql-server/server"
"github.com/jackc/pgx/v5/pgproto3"
)
var (
terminate = &sync.WaitGroup{} // terminate is used when a Terminate message has been received.
messageMutex = &sync.Mutex{} // messageMutex guards against both the client and server writing to the message slice.
allMessages = make([]pgproto3.Message, 0, 1000000) // allMessages contains all messages exchanged by the client and server.
)
func main() {
// If no arguments are given, then we'll update the results against the regression files
if len(os.Args) <= 1 {
updateResults()
return
} else if len(os.Args) != 3 {
fmt.Println("Expected two arguments, each containing a file name pointing to the tracker files (located in the out directory)")
os.Exit(1)
}
trackersFrom, err := regressionFolder.ReadReplayTrackers("out/" + os.Args[1])
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
trackersTo, err := regressionFolder.ReadReplayTrackers("out/" + os.Args[2])
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
fromTotal := uint32(0)
fromSuccess := uint32(0)
fromPartial := uint32(0)
fromFail := uint32(0)
for _, tracker := range trackersFrom {
fromTotal += tracker.Success
fromTotal += tracker.Failed
fromSuccess += tracker.Success
fromPartial += tracker.PartialSuccess
fromFail += tracker.Failed
}
toTotal := uint32(0)
toSuccess := uint32(0)
toPartial := uint32(0)
toFail := uint32(0)
for _, tracker := range trackersTo {
toTotal += tracker.Success
toTotal += tracker.Failed
toSuccess += tracker.Success
toPartial += tracker.PartialSuccess
toFail += tracker.Failed
}
sb := strings.Builder{}
sb.WriteString("| | Main | PR |\n")
sb.WriteString("| --- | --- | --- |\n")
sb.WriteString(fmt.Sprintf("| Total | %d | %d |\n", fromTotal, toTotal))
sb.WriteString(fmt.Sprintf("| Successful | %d | %d |\n", fromSuccess, toSuccess))
sb.WriteString(fmt.Sprintf("| Failures | %d | %d |\n", fromFail, toFail))
sb.WriteString(fmt.Sprintf("| Partial Successes[^1] | %d | %d |\n", fromPartial, toPartial))
sb.WriteString("\n| | Main | PR |\n")
sb.WriteString("| --- | --- | --- |\n")
sb.WriteString(fmt.Sprintf("| Successful | %.4f%% | %.4f%% |\n",
(float64(fromSuccess)/float64(fromTotal))*100.0,
(float64(toSuccess)/float64(toTotal))*100.0))
sb.WriteString(fmt.Sprintf("| Failures | %.4f%% | %.4f%% |\n",
(float64(fromFail)/float64(fromTotal))*100.0,
(float64(toFail)/float64(toTotal))*100.0))
totalRegressions := 0
totalProgressions := 0
if len(trackersFrom) == len(trackersTo) {
// Handle regressions (which we'll display first)
foundAnyFailDiff := false
for trackerIdx := range trackersFrom {
// They're sorted, so this should always hold true.
// This will really only fail if the tests were updated.
if trackersFrom[trackerIdx].File != trackersTo[trackerIdx].File {
continue
}
foundFileDiff := false
fromFailItems := make(map[string]struct{})
for _, trackerFromItem := range trackersFrom[trackerIdx].FailPartialItems {
fromFailItems[trackerFromItem.Query] = struct{}{}
}
for _, trackerToItem := range trackersTo[trackerIdx].FailPartialItems {
if _, ok := fromFailItems[trackerToItem.Query]; !ok {
if totalRegressions < 40 {
if !foundAnyFailDiff {
foundAnyFailDiff = true
sb.WriteString("\n## ${\\color{red}Regressions__&&&&&&}$\n")
}
if !foundFileDiff {
foundFileDiff = true
sb.WriteString(fmt.Sprintf("### %s\n", trackersFrom[trackerIdx].File))
}
sb.WriteString(fmt.Sprintf("```\nQUERY: %s\n", trackerToItem.Query))
if len(trackerToItem.ExpectedError) != 0 {
sb.WriteString(fmt.Sprintf("EXPECTED ERROR: %s\n", trackerToItem.ExpectedError))
}
if len(trackerToItem.UnexpectedError) != 0 {
sb.WriteString(fmt.Sprintf("RECEIVED ERROR: %s\n", trackerToItem.UnexpectedError))
}
for _, partial := range trackerToItem.PartialSuccess {
sb.WriteString(fmt.Sprintf("PARTIAL: %s\n", partial))
}
sb.WriteString("```\n")
}
totalRegressions += 1
}
}
}
// Handle progressions (which we'll display second)
foundAnySuccessDiff := false
for trackerIdx := range trackersFrom {
// They're sorted, so this should always hold true.
// This will really only fail if the tests were updated.
if trackersFrom[trackerIdx].File != trackersTo[trackerIdx].File {
continue
}
foundFileDiff := false
fromSuccessItems := make(map[string]struct{})
for _, trackerFromItem := range trackersFrom[trackerIdx].SuccessItems {
fromSuccessItems[trackerFromItem.Query] = struct{}{}
}
for _, trackerToItem := range trackersTo[trackerIdx].SuccessItems {
if _, ok := fromSuccessItems[trackerToItem.Query]; !ok {
if totalProgressions < 40 {
if !foundAnySuccessDiff {
foundAnySuccessDiff = true
sb.WriteString("\n## ${\\color{lightgreen}Progressions__&&&&&&}$\n")
}
if !foundFileDiff {
foundFileDiff = true
sb.WriteString(fmt.Sprintf("### %s\n", trackersFrom[trackerIdx].File))
}
sb.WriteString(fmt.Sprintf("```\nQUERY: %s\n```\n", trackerToItem.Query))
}
totalProgressions += 1
}
}
}
}
sb.WriteString("[^1]: These are tests that we're marking as `Successful`, however they do not match the expected output in some way. This is due to small differences, such as different wording on the error messages, or the column names being incorrect while the data itself is correct.")
output := sb.String()
output = strings.ReplaceAll(output, "Regressions__&&&&&&", fmt.Sprintf("Regressions (%d)", totalRegressions))
output = strings.ReplaceAll(output, "Progressions__&&&&&&", fmt.Sprintf("Progressions (%d)", totalProgressions))
fmt.Println(output)
}
func updateResults() {
fmt.Println("Updating results, remember to run the regression tester using our schedule")
listener, err := server.NewListener("tcp", "127.0.0.1:5431", "")
if err != nil {
fmt.Println(err)
return
}
timer := time.NewTimer(5 * time.Second)
timer.Stop()
go func() {
<-timer.C
_ = listener.Close()
}()
for {
clientConn, err := listener.Accept()
if err != nil {
break
}
timer.Stop()
terminate = &sync.WaitGroup{}
terminate.Add(1)
clientConnBackend := pgproto3.NewBackend(clientConn, clientConn)
postgresConn, err := (&net.Dialer{}).Dial("tcp", "127.0.0.1:5432")
if err != nil {
fmt.Println(err)
return
}
postgresConnFrontend := pgproto3.NewFrontend(postgresConn, postgresConn)
if err = handleStartup(clientConnBackend, postgresConnFrontend, clientConn); err != nil {
fmt.Println(err)
return
}
createPassthrough(clientConnBackend, postgresConnFrontend)
terminate.Wait()
_ = clientConn.Close()
_ = postgresConn.Close()
timer.Reset(5 * time.Second)
}
// The first two groups are a part of the setup, so we'll combine them with the first file (which is the setup)
groups := SplitByTerminate(allMessages)
groups[2] = CombineGroups(groups[0], groups[1], groups[2])
groups = groups[2:]
if len(groups) != len(AllTestResultFilesNames) {
fmt.Printf("Number of groups: %d\nNumber of test names: %d", len(groups), len(AllTestResultFilesNames))
return
}
for i := 0; i < len(groups); i++ {
if err = regressionFolder.WriteMessages(AllTestResultFilesNames[i], groups[i], 0644); err != nil {
fmt.Println(err)
return
}
}
}
// addMessage adds the given message to the message slice.
func addMessage(message pgproto3.Message) {
messageMutex.Lock()
defer messageMutex.Unlock()
allMessages = append(allMessages, message)
fmt.Printf("%T: %v\n", message, message)
}
@@ -0,0 +1,168 @@
// Copyright 2024 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 main
import (
"github.com/cockroachdb/errors"
"github.com/jackc/pgx/v5/pgproto3"
)
// EncodeMessage encodes the given message to the byte representation needed by the Decode function. Each message's
// Encode function will often include additional information that the Decode function does not expect, hence it is
// removed in this function.
func EncodeMessage(message pgproto3.Message) ([]byte, error) {
switch message := message.(type) {
case *pgproto3.AuthenticationCleartextPassword:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.AuthenticationGSS:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.AuthenticationGSSContinue:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.AuthenticationMD5Password:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.AuthenticationOk:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.AuthenticationSASL:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.AuthenticationSASLContinue:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.AuthenticationSASLFinal:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.BackendKeyData:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.Bind:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.BindComplete:
return nil, nil
case *pgproto3.CancelRequest:
data, err := message.Encode(nil)
return data[4:], err
case *pgproto3.Close:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.CloseComplete:
return nil, nil
case *pgproto3.CommandComplete:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.CopyBothResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.CopyData:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.CopyDone:
return nil, nil
case *pgproto3.CopyFail:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.CopyInResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.CopyOutResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.DataRow:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.Describe:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.EmptyQueryResponse:
return nil, nil
case *pgproto3.ErrorResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.Execute:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.Flush:
return nil, nil
case *pgproto3.FunctionCall:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.FunctionCallResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.GSSEncRequest:
data, err := message.Encode(nil)
return data[4:], err
case *pgproto3.GSSResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.NoData:
return nil, nil
case *pgproto3.NoticeResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.NotificationResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.ParameterDescription:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.ParameterStatus:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.Parse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.ParseComplete:
return nil, nil
case *pgproto3.PasswordMessage:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.PortalSuspended:
return nil, nil
case *pgproto3.Query:
data, err := RewriteCopyToFileOnly(message).Encode(nil)
return data[5:], err
case *pgproto3.ReadyForQuery:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.RowDescription:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.SASLInitialResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.SASLResponse:
data, err := message.Encode(nil)
return data[5:], err
case *pgproto3.SSLRequest:
data, err := message.Encode(nil)
return data[4:], err
case *pgproto3.StartupMessage:
data, err := message.Encode(nil)
return data[4:], err
case *pgproto3.Sync:
return nil, nil
case *pgproto3.Terminate:
return nil, nil
default:
return nil, errors.Errorf("unknown message type: %T", message)
}
}
@@ -0,0 +1,120 @@
// Copyright 2024 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 main
import "github.com/jackc/pgx/v5/pgproto3"
// MessageReader acts like an iterator over a collection of messages.
type MessageReader struct {
messages []pgproto3.Message
queue []pgproto3.Message
idx int
}
// NewMessageReader returns a new *MessageReader.
func NewMessageReader(messages []pgproto3.Message) *MessageReader {
return &MessageReader{
messages: messages,
idx: 0,
}
}
// Decrement sets the reader back a position, so that the next call to Next will return the previous message.
func (mr *MessageReader) Decrement() {
mr.idx--
if mr.idx < 0 {
mr.idx = 0
}
}
// IsEmpty returns true when all messages have been returned.
func (mr *MessageReader) IsEmpty() bool {
return len(mr.queue) == 0 && mr.idx >= len(mr.messages)
}
// Next returns the next message, or nil if the reader has been exhausted.
func (mr *MessageReader) Next() pgproto3.Message {
if len(mr.queue) > 0 {
if len(mr.queue) == 1 {
m := mr.queue[0]
mr.queue = nil
return m
} else {
m := mr.queue[0]
mr.queue = mr.queue[1:]
return m
}
}
if mr.idx >= len(mr.messages) {
return nil
}
mr.idx++
return mr.messages[mr.idx-1]
}
// Peek returns the next message without moving the reader forward, or nil if the reader has been exhausted.
func (mr *MessageReader) Peek() pgproto3.Message {
if len(mr.queue) > 0 {
return mr.queue[0]
}
if mr.idx >= len(mr.messages) {
return nil
}
return mr.messages[mr.idx]
}
// Previous returns the message that was last returned by Next. Returns nil if the reader is at the beginning message.
func (mr *MessageReader) Previous() pgproto3.Message {
if mr.idx <= 0 {
return nil
}
return mr.messages[mr.idx-1]
}
// PushQueue adds the given messages to the queue, which will be returned when calling Next. Both Previous and Decrement
// ignore the queue.
func (mr *MessageReader) PushQueue(messages ...pgproto3.Message) {
mr.queue = append(mr.queue, messages...)
}
// SurroundingMessages returns the messages that are surrounding the ones at the current index. This is primarily for
// debugging, as it can be hard to locate messages when there are thousands in the reader, and you want to know which
// messages surround the current one.
func (mr *MessageReader) SurroundingMessages(amountBefore int, amountAfter int) []pgproto3.Message {
amountBefore = mr.idx - amountBefore
amountAfter = mr.idx + amountAfter
if amountBefore < 0 {
amountBefore = 0
}
if amountAfter > len(mr.messages) {
amountAfter = len(mr.messages) - 1
}
return mr.messages[amountBefore:amountAfter]
}
// SyncToNextQuery advances the reader forward to the next query.
func (mr *MessageReader) SyncToNextQuery() {
for {
switch mr.Next().(type) {
case *pgproto3.ReadyForQuery:
return
case *pgproto3.Terminate:
mr.Decrement()
return
case nil:
return
}
}
}
+312
View File
@@ -0,0 +1,312 @@
// Copyright 2024 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 main
import (
"github.com/cockroachdb/errors"
"github.com/jackc/pgx/v5/pgproto3"
)
// MessageType represents the type of a message. New message types should be appended to the end, as we depend on this
// exact order for the serialized files.
type MessageType uint16
const (
MessageType_AuthenticationCleartextPassword = iota
MessageType_AuthenticationGSS
MessageType_AuthenticationGSSContinue
MessageType_AuthenticationMD5Password
MessageType_AuthenticationOk
MessageType_AuthenticationSASL
MessageType_AuthenticationSASLContinue
MessageType_AuthenticationSASLFinal
MessageType_BackendKeyData
MessageType_Bind
MessageType_BindComplete
MessageType_CancelRequest
MessageType_Close
MessageType_CloseComplete
MessageType_CommandComplete
MessageType_CopyBothResponse
MessageType_CopyData
MessageType_CopyDone
MessageType_CopyFail
MessageType_CopyInResponse
MessageType_CopyOutResponse
MessageType_DataRow
MessageType_Describe
MessageType_EmptyQueryResponse
MessageType_ErrorResponse
MessageType_Execute
MessageType_Flush
MessageType_FunctionCall
MessageType_FunctionCallResponse
MessageType_GSSEncRequest
MessageType_GSSResponse
MessageType_NoData
MessageType_NoticeResponse
MessageType_NotificationResponse
MessageType_ParameterDescription
MessageType_ParameterStatus
MessageType_Parse
MessageType_ParseComplete
MessageType_PasswordMessage
MessageType_PortalSuspended
MessageType_Query
MessageType_ReadyForQuery
MessageType_RowDescription
MessageType_SASLInitialResponse
MessageType_SASLResponse
MessageType_SSLRequest
MessageType_StartupMessage
MessageType_Sync
MessageType_Terminate
)
// ToMessageType returns the MessageType of the given message.
func ToMessageType(message pgproto3.Message) (MessageType, error) {
switch message.(type) {
case *pgproto3.AuthenticationCleartextPassword:
return MessageType_AuthenticationCleartextPassword, nil
case *pgproto3.AuthenticationGSS:
return MessageType_AuthenticationGSS, nil
case *pgproto3.AuthenticationGSSContinue:
return MessageType_AuthenticationGSSContinue, nil
case *pgproto3.AuthenticationMD5Password:
return MessageType_AuthenticationMD5Password, nil
case *pgproto3.AuthenticationOk:
return MessageType_AuthenticationOk, nil
case *pgproto3.AuthenticationSASL:
return MessageType_AuthenticationSASL, nil
case *pgproto3.AuthenticationSASLContinue:
return MessageType_AuthenticationSASLContinue, nil
case *pgproto3.AuthenticationSASLFinal:
return MessageType_AuthenticationSASLFinal, nil
case *pgproto3.BackendKeyData:
return MessageType_BackendKeyData, nil
case *pgproto3.Bind:
return MessageType_Bind, nil
case *pgproto3.BindComplete:
return MessageType_BindComplete, nil
case *pgproto3.CancelRequest:
return MessageType_CancelRequest, nil
case *pgproto3.Close:
return MessageType_Close, nil
case *pgproto3.CloseComplete:
return MessageType_CloseComplete, nil
case *pgproto3.CommandComplete:
return MessageType_CommandComplete, nil
case *pgproto3.CopyBothResponse:
return MessageType_CopyBothResponse, nil
case *pgproto3.CopyData:
return MessageType_CopyData, nil
case *pgproto3.CopyDone:
return MessageType_CopyDone, nil
case *pgproto3.CopyFail:
return MessageType_CopyFail, nil
case *pgproto3.CopyInResponse:
return MessageType_CopyInResponse, nil
case *pgproto3.CopyOutResponse:
return MessageType_CopyOutResponse, nil
case *pgproto3.DataRow:
return MessageType_DataRow, nil
case *pgproto3.Describe:
return MessageType_Describe, nil
case *pgproto3.EmptyQueryResponse:
return MessageType_EmptyQueryResponse, nil
case *pgproto3.ErrorResponse:
return MessageType_ErrorResponse, nil
case *pgproto3.Execute:
return MessageType_Execute, nil
case *pgproto3.Flush:
return MessageType_Flush, nil
case *pgproto3.FunctionCall:
return MessageType_FunctionCall, nil
case *pgproto3.FunctionCallResponse:
return MessageType_FunctionCallResponse, nil
case *pgproto3.GSSEncRequest:
return MessageType_GSSEncRequest, nil
case *pgproto3.GSSResponse:
return MessageType_GSSResponse, nil
case *pgproto3.NoData:
return MessageType_NoData, nil
case *pgproto3.NoticeResponse:
return MessageType_NoticeResponse, nil
case *pgproto3.NotificationResponse:
return MessageType_NotificationResponse, nil
case *pgproto3.ParameterDescription:
return MessageType_ParameterDescription, nil
case *pgproto3.ParameterStatus:
return MessageType_ParameterStatus, nil
case *pgproto3.Parse:
return MessageType_Parse, nil
case *pgproto3.ParseComplete:
return MessageType_ParseComplete, nil
case *pgproto3.PasswordMessage:
return MessageType_PasswordMessage, nil
case *pgproto3.PortalSuspended:
return MessageType_PortalSuspended, nil
case *pgproto3.Query:
return MessageType_Query, nil
case *pgproto3.ReadyForQuery:
return MessageType_ReadyForQuery, nil
case *pgproto3.RowDescription:
return MessageType_RowDescription, nil
case *pgproto3.SASLInitialResponse:
return MessageType_SASLInitialResponse, nil
case *pgproto3.SASLResponse:
return MessageType_SASLResponse, nil
case *pgproto3.SSLRequest:
return MessageType_SSLRequest, nil
case *pgproto3.StartupMessage:
return MessageType_StartupMessage, nil
case *pgproto3.Sync:
return MessageType_Sync, nil
case *pgproto3.Terminate:
return MessageType_Terminate, nil
default:
return 0, errors.Errorf("unknown message type: %T", message)
}
}
// FromMessageType returns a new message (ready to be decoded into) from the given MessageType.
func FromMessageType(messageType MessageType) (pgproto3.Message, error) {
switch messageType {
case MessageType_AuthenticationCleartextPassword:
return &pgproto3.AuthenticationCleartextPassword{}, nil
case MessageType_AuthenticationGSS:
return &pgproto3.AuthenticationGSS{}, nil
case MessageType_AuthenticationGSSContinue:
return &pgproto3.AuthenticationGSSContinue{}, nil
case MessageType_AuthenticationMD5Password:
return &pgproto3.AuthenticationMD5Password{}, nil
case MessageType_AuthenticationOk:
return &pgproto3.AuthenticationOk{}, nil
case MessageType_AuthenticationSASL:
return &pgproto3.AuthenticationSASL{}, nil
case MessageType_AuthenticationSASLContinue:
return &pgproto3.AuthenticationSASLContinue{}, nil
case MessageType_AuthenticationSASLFinal:
return &pgproto3.AuthenticationSASLFinal{}, nil
case MessageType_BackendKeyData:
return &pgproto3.BackendKeyData{}, nil
case MessageType_Bind:
return &pgproto3.Bind{}, nil
case MessageType_BindComplete:
return &pgproto3.BindComplete{}, nil
case MessageType_CancelRequest:
return &pgproto3.CancelRequest{}, nil
case MessageType_Close:
return &pgproto3.Close{}, nil
case MessageType_CloseComplete:
return &pgproto3.CloseComplete{}, nil
case MessageType_CommandComplete:
return &pgproto3.CommandComplete{}, nil
case MessageType_CopyBothResponse:
return &pgproto3.CopyBothResponse{}, nil
case MessageType_CopyData:
return &pgproto3.CopyData{}, nil
case MessageType_CopyDone:
return &pgproto3.CopyDone{}, nil
case MessageType_CopyFail:
return &pgproto3.CopyFail{}, nil
case MessageType_CopyInResponse:
return &pgproto3.CopyInResponse{}, nil
case MessageType_CopyOutResponse:
return &pgproto3.CopyOutResponse{}, nil
case MessageType_DataRow:
return &pgproto3.DataRow{}, nil
case MessageType_Describe:
return &pgproto3.Describe{}, nil
case MessageType_EmptyQueryResponse:
return &pgproto3.EmptyQueryResponse{}, nil
case MessageType_ErrorResponse:
return &pgproto3.ErrorResponse{}, nil
case MessageType_Execute:
return &pgproto3.Execute{}, nil
case MessageType_Flush:
return &pgproto3.Flush{}, nil
case MessageType_FunctionCall:
return &pgproto3.FunctionCall{}, nil
case MessageType_FunctionCallResponse:
return &pgproto3.FunctionCallResponse{}, nil
case MessageType_GSSEncRequest:
return &pgproto3.GSSEncRequest{}, nil
case MessageType_GSSResponse:
return &pgproto3.GSSResponse{}, nil
case MessageType_NoData:
return &pgproto3.NoData{}, nil
case MessageType_NoticeResponse:
return &pgproto3.NoticeResponse{}, nil
case MessageType_NotificationResponse:
return &pgproto3.NotificationResponse{}, nil
case MessageType_ParameterDescription:
return &pgproto3.ParameterDescription{}, nil
case MessageType_ParameterStatus:
return &pgproto3.ParameterStatus{}, nil
case MessageType_Parse:
return &pgproto3.Parse{}, nil
case MessageType_ParseComplete:
return &pgproto3.ParseComplete{}, nil
case MessageType_PasswordMessage:
return &pgproto3.PasswordMessage{}, nil
case MessageType_PortalSuspended:
return &pgproto3.PortalSuspended{}, nil
case MessageType_Query:
return &pgproto3.Query{}, nil
case MessageType_ReadyForQuery:
return &pgproto3.ReadyForQuery{}, nil
case MessageType_RowDescription:
return &pgproto3.RowDescription{}, nil
case MessageType_SASLInitialResponse:
return &pgproto3.SASLInitialResponse{}, nil
case MessageType_SASLResponse:
return &pgproto3.SASLResponse{}, nil
case MessageType_SSLRequest:
return &pgproto3.SSLRequest{}, nil
case MessageType_StartupMessage:
return &pgproto3.StartupMessage{}, nil
case MessageType_Sync:
return &pgproto3.Sync{}, nil
case MessageType_Terminate:
return &pgproto3.Terminate{}, nil
default:
return nil, errors.Errorf("unknown message type: %d", uint16(messageType))
}
}
// DuplicateMessage returns a duplicate of the given message, since the connections generally reuse the messages.
func DuplicateMessage(message pgproto3.Message) pgproto3.Message {
if message == nil {
return message
}
messageType, err := ToMessageType(message)
if err != nil {
return message
}
newMessage, err := FromMessageType(messageType)
if err != nil {
return message
}
data, err := EncodeMessage(message)
if err != nil {
return message
}
if err = newMessage.Decode(data); err != nil {
return message
}
return newMessage
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2024 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 main
import (
"fmt"
"strings"
"github.com/jackc/pgx/v5/pgproto3"
)
// createPassthrough creates the go routines that will read from and write to the connections.
func createPassthrough(clientConnBackend *pgproto3.Backend, postgresConnFrontend *pgproto3.Frontend) {
go func() {
for {
clientMessage, err := clientConnBackend.Receive()
if err != nil {
errStr := err.Error()
if errStr != "unexpected EOF" && !strings.HasSuffix(errStr, "use of closed network connection") {
fmt.Println(err)
}
return
}
clientMessage = DuplicateMessage(clientMessage).(pgproto3.FrontendMessage)
if query, ok := clientMessage.(*pgproto3.Query); ok {
clientMessage, err = RewriteCopyToLocal(query)
if err != nil {
panic(err)
}
}
addMessage(clientMessage)
if _, ok := clientMessage.(*pgproto3.Terminate); ok {
terminate.Done()
return
}
postgresConnFrontend.Send(clientMessage)
if err = postgresConnFrontend.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 {
postgresMessage, err := postgresConnFrontend.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
}
postgresMessage = DuplicateMessage(postgresMessage).(pgproto3.BackendMessage)
addMessage(postgresMessage)
if err = setAuthType(clientConnBackend, postgresMessage); err != nil {
fmt.Println(err)
return
}
clientConnBackend.Send(postgresMessage)
if err = clientConnBackend.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
}
}
}()
}
@@ -0,0 +1,169 @@
// Copyright 2024 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 main
import (
"os"
"path/filepath"
"runtime"
"github.com/cockroachdb/errors"
"github.com/jackc/pgx/v5/pgproto3"
"github.com/dolthub/doltgresql/utils"
)
var regressionFolder RegressionFolderLocation // regressionFolder is the disk location of the regression folder
// RegressionFolderLocation is the location of this project's root folder.
type RegressionFolderLocation struct {
path string
}
// GetRegressionFolder returns the location of the regression folder (testing/go/regression). This is used to find the
// absolute position of our data files.
func GetRegressionFolder() (RegressionFolderLocation, error) {
_, currentFileLocation, _, ok := runtime.Caller(0)
if !ok {
return RegressionFolderLocation{}, errors.Errorf("failed to fetch the location of the current file")
}
regressionFolder = RegressionFolderLocation{filepath.Clean(filepath.Join(filepath.Dir(currentFileLocation), ".."))}
return regressionFolder, nil
}
// MoveRoot returns a new RegressionFolderLocation that defines the root at the new path. The parameter should be relative to
// the current root.
func (root RegressionFolderLocation) MoveRoot(relativePath string) RegressionFolderLocation {
return RegressionFolderLocation{filepath.Clean(filepath.Join(root.path, relativePath))}
}
// GetAbsolutePath returns the absolute path of the given path, which should be relative to the project's root
// folder.
func (root RegressionFolderLocation) GetAbsolutePath(relativePath string) string {
return filepath.ToSlash(filepath.Join(root.path, relativePath))
}
// Exists returns whether the file or directory at the given path (relative to the root path) exists. Returns an error
// if the check was unable to be completed.
func (root RegressionFolderLocation) Exists(relativePath string) (bool, error) {
_, err := os.Stat(root.GetAbsolutePath(relativePath))
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}
// ReadDir is equivalent to os.ReadDir, except that it uses the root path and the given relative path.
func (root RegressionFolderLocation) ReadDir(relativePath string) ([]os.DirEntry, error) {
return os.ReadDir(root.GetAbsolutePath(relativePath))
}
// ReadFile is equivalent to os.ReadFile, except that it uses the root path and the given relative path.
func (root RegressionFolderLocation) ReadFile(relativePath string) ([]byte, error) {
return os.ReadFile(root.GetAbsolutePath(relativePath))
}
// ReadMessages reads the messages from the file at the given path (relative to the root path). It is assumed that this
// file was previously written to using WriteMessages.
func (root RegressionFolderLocation) ReadMessages(relativePath string) ([]pgproto3.Message, error) {
fileData, err := root.ReadFile(relativePath)
if err != nil {
return nil, err
}
reader := utils.NewReader(fileData)
messages := make([]pgproto3.Message, reader.Uint32())
for i := 0; i < len(messages); i++ {
var err error
messages[i], err = FromMessageType(MessageType(reader.Uint16()))
if err != nil {
return nil, err
}
if err = messages[i].Decode(reader.ByteSlice()); err != nil {
return nil, err
}
if query, ok := messages[i].(*pgproto3.Query); ok {
messages[i], err = RewriteCopyToLocal(query)
if err != nil {
return nil, err
}
}
}
if !reader.IsEmpty() {
return nil, errors.Errorf("file has additional data")
}
return messages, nil
}
// ReadReplayTrackers reads the replay trackers from the file at the given path (relative to the root path). It is
// assumed that this file was previously written to using WriteReplayTrackers.
func (root RegressionFolderLocation) ReadReplayTrackers(relativePath string) ([]*ReplayTracker, error) {
fileData, err := root.ReadFile(relativePath)
if err != nil {
return nil, err
}
return DeserializeTrackers(fileData)
}
// WriteFile is equivalent to os.WriteFile, except that it uses the root path and the given relative path.
func (root RegressionFolderLocation) WriteFile(relativePath string, data []byte, perm os.FileMode) error {
directory := filepath.ToSlash(filepath.Dir(relativePath))
exists, err := root.Exists(directory)
if err != nil {
return err
}
if !exists {
if err = os.MkdirAll(root.GetAbsolutePath(directory), 0644); err != nil {
return err
}
}
return os.WriteFile(root.GetAbsolutePath(relativePath), data, perm)
}
// WriteMessages writes the given messages to the file at the given path (relative to the root path). It is assumed that
// this file will be read using ReadMessages.
func (root RegressionFolderLocation) WriteMessages(relativePath string, messages []pgproto3.Message, perm os.FileMode) error {
writer := utils.NewWriter(1048576)
writer.Uint32(uint32(len(messages)))
for _, message := range messages {
messageType, err := ToMessageType(message)
if err != nil {
return err
}
writer.Uint16(uint16(messageType))
data, err := EncodeMessage(message)
if err != nil {
return err
}
writer.ByteSlice(data)
}
return root.WriteFile(relativePath, writer.Data(), perm)
}
// WriteReplayTrackers writes the given replay trackers to the file at the given path (relative to the root path). It is
// assumed that this file will be read using ReadReplayTrackers.
func (root RegressionFolderLocation) WriteReplayTrackers(relativePath string, trackers []*ReplayTracker, perm os.FileMode) error {
return root.WriteFile(relativePath, SerializeTrackers(trackers...), perm)
}
// init is used to load the location of the regression folder.
func init() {
var err error
regressionFolder, err = GetRegressionFolder()
if err != nil {
panic(err)
}
}
+680
View File
@@ -0,0 +1,680 @@
// Copyright 2024 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 main
import (
"bytes"
"fmt"
"strconv"
"strings"
"time"
"github.com/cockroachdb/errors"
"github.com/jackc/pgx/v5/pgproto3"
)
// ReplayOptions contain all of the options that may be given to Replay. This is a replacement for a long argument list.
type ReplayOptions struct {
File string
Port int
Messages []pgproto3.Message
PrintQueries bool // Prints both queries and file names to the CLI
FailPSQL bool // Whether we should automatically fail PSQL commands, since they're slow and we fail them anyway
FailQueries []string // These are queries that cause catastrophic failures, like OOM errors, stack limits, etc.
}
// Replay will replay the given messages onto the Doltgres server running on the given port.
func Replay(options ReplayOptions) (*ReplayTracker, error) {
tracker := NewReplayTracker(options.File)
reader := NewMessageReader(FilterMessages(options.Messages))
t := time.Now()
fmt.Println("-------------------- ", tracker.File, " --------------------")
ListenerLoop:
for !reader.IsEmpty() {
connection, err := NewConnection("127.0.0.1:"+strconv.Itoa(options.Port), reader, 15*time.Second)
if err != nil {
return nil, err
}
startupMessage, ok := reader.Next().(*pgproto3.StartupMessage)
if !ok {
return nil, errors.Errorf("%s: first message is not StartupMessage (%T)", options.File, reader.Previous())
}
if _, ok = reader.Next().(*pgproto3.ReadyForQuery); !ok {
return nil, errors.Errorf("expected message after StartupMessage to be ReadyForQuery (%T)", reader.Previous())
}
if err = connection.SendNoSync(startupMessage); err != nil {
return nil, err
}
StartupLoop:
for {
postgresMessage, err := connection.Receive()
if err != nil {
return nil, err
}
switch response := postgresMessage.(type) {
case *pgproto3.AuthenticationOk:
case *pgproto3.BackendKeyData:
case *pgproto3.ErrorResponse:
return nil, errors.New(response.Message)
case *pgproto3.ParameterStatus:
case *pgproto3.ReadyForQuery:
break StartupLoop
default:
return nil, errors.Errorf("unknown StartupMessage response type: %T", response)
}
}
MessageLoop:
for message := reader.Next(); message != nil; message = reader.Next() {
switch message := message.(type) {
case *pgproto3.CopyData:
// TODO: messages have somehow gotten misordered in `copy2`, so need to fix that, then can remove this case
reader.SyncToNextQuery()
case *pgproto3.Describe:
connection.Queue(message)
if sync, ok := reader.Peek().(*pgproto3.Sync); ok {
_ = reader.Next()
connection.Queue(sync)
}
if err = connection.Send(); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: "DESCRIBE",
UnexpectedError: err.Error(),
})
continue ListenerLoop
}
var expectedError *pgproto3.ErrorResponse
var expectedRowDesc *pgproto3.RowDescription
DescribeLoop:
for {
switch queryMessage := reader.Next().(type) {
case *pgproto3.EmptyQueryResponse:
case *pgproto3.ErrorResponse:
expectedError = queryMessage
case *pgproto3.NoData:
case *pgproto3.ReadyForQuery:
break DescribeLoop
case *pgproto3.RowDescription:
expectedRowDesc = queryMessage
default:
return nil, errors.Errorf("unable to determine what to do with %T", queryMessage)
}
}
var responseError *pgproto3.ErrorResponse
var responseRowDesc *pgproto3.RowDescription
DescribeResponseLoop:
for {
response, err := connection.Receive()
if err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: "DESCRIBE",
UnexpectedError: err.Error(),
})
continue ListenerLoop
}
response = DuplicateMessage(response).(pgproto3.BackendMessage)
switch response := response.(type) {
case *pgproto3.EmptyQueryResponse:
case *pgproto3.ErrorResponse:
responseError = response
case *pgproto3.NoData:
case *pgproto3.NoticeResponse:
case *pgproto3.ParameterDescription:
case *pgproto3.ReadyForQuery:
break DescribeResponseLoop
case *pgproto3.RowDescription:
responseRowDesc = response
default:
return nil, errors.Errorf("unable to determine what to do with %T", message)
}
}
if err = connection.EmptyReceiveBuffer(); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: "DESCRIBE",
UnexpectedError: err.Error(),
})
continue MessageLoop
}
if expectedError == nil {
if responseError != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: "DESCRIBE",
UnexpectedError: responseError.Message,
})
continue MessageLoop
}
if expectedRowDesc == nil {
if responseRowDesc == nil {
tracker.Success++
} else {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: "DESCRIBE",
UnexpectedError: "expected no row description but received a description",
})
}
continue MessageLoop
}
if responseRowDesc == nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: "DESCRIBE",
UnexpectedError: "expected rows but received none",
})
continue MessageLoop
}
if len(expectedRowDesc.Fields) != len(responseRowDesc.Fields) {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: "DESCRIBE",
UnexpectedError: fmt.Sprintf("expected column count %d but received %d",
len(expectedRowDesc.Fields), len(responseRowDesc.Fields)),
})
continue MessageLoop
}
var partialSuccesses []string
for i := range expectedRowDesc.Fields {
expectedName := string(expectedRowDesc.Fields[i].Name)
responseName := string(responseRowDesc.Fields[i].Name)
if expectedName != responseName {
partialSuccesses = append(partialSuccesses,
fmt.Sprintf("expected column with name `%s` but received `%s`", expectedName, responseName))
}
// TODO: determine if we should also check column types
}
tracker.Success++
if len(partialSuccesses) > 0 {
tracker.PartialSuccess++
}
} else /* expectedError != nil */ {
if responseError == nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: "DESCRIBE",
ExpectedError: expectedError.Message,
})
continue MessageLoop
}
tracker.Success++
if expectedError.Message != responseError.Message {
tracker.PartialSuccess++
tracker.AddFailure(ReplayTrackerItem{
Query: "DESCRIBE",
UnexpectedError: responseError.Message,
ExpectedError: expectedError.Message,
})
}
}
case *pgproto3.FunctionCall:
if err = connection.Send(message); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
UnexpectedError: err.Error(),
})
continue ListenerLoop
}
var expectedError *pgproto3.ErrorResponse
var expectedData *pgproto3.FunctionCallResponse
FunctionLoop:
for {
switch queryMessage := reader.Next().(type) {
case *pgproto3.ErrorResponse:
expectedError = queryMessage
case *pgproto3.FunctionCallResponse:
expectedData = queryMessage
case *pgproto3.ReadyForQuery:
break FunctionLoop
default:
return nil, errors.Errorf("unable to determine what to do with %T", queryMessage)
}
}
var responseError *pgproto3.ErrorResponse
var responseData *pgproto3.FunctionCallResponse
FunctionResponseLoop:
for {
response, err := connection.Receive()
if err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
UnexpectedError: err.Error(),
})
continue ListenerLoop
}
response = DuplicateMessage(response).(pgproto3.BackendMessage)
switch response := response.(type) {
case *pgproto3.EmptyQueryResponse:
case *pgproto3.ErrorResponse:
responseError = response
case *pgproto3.FunctionCallResponse:
responseData = response
case *pgproto3.NoticeResponse:
case *pgproto3.ReadyForQuery:
break FunctionResponseLoop
default:
return nil, errors.Errorf("unable to determine what to do with %T", message)
}
}
if err = connection.EmptyReceiveBuffer(); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
UnexpectedError: err.Error(),
})
continue MessageLoop
}
if expectedError == nil {
if responseError != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
UnexpectedError: responseError.Message,
})
continue MessageLoop
}
if expectedData != nil {
if responseData == nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
UnexpectedError: "expected a result but received no result",
})
continue MessageLoop
}
if !bytes.Equal(expectedData.Result, responseData.Result) {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
UnexpectedError: "result is incorrect",
})
continue MessageLoop
}
} else /* expectedData == nil */ {
if responseData != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
UnexpectedError: "expected no result but received a result",
})
continue MessageLoop
}
}
tracker.Success++
tracker.AddSuccess(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
})
} else /* expectedError != nil */ {
if responseError == nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
ExpectedError: expectedError.Message,
})
continue MessageLoop
}
tracker.Success++
tracker.AddSuccess(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
})
if expectedError.Message != responseError.Message {
tracker.PartialSuccess++
tracker.AddFailure(ReplayTrackerItem{
Query: fmt.Sprintf("Function OID: %d", message.Function),
UnexpectedError: responseError.Message,
ExpectedError: expectedError.Message,
})
}
}
case *pgproto3.Parse:
connection.Queue(message)
if sync, ok := reader.Peek().(*pgproto3.Sync); ok {
_ = reader.Next()
connection.Queue(sync)
}
if err = connection.Send(); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.Query,
UnexpectedError: err.Error(),
})
continue ListenerLoop
}
var expectedError *pgproto3.ErrorResponse
ParseLoop:
for {
switch queryMessage := reader.Next().(type) {
case *pgproto3.EmptyQueryResponse:
case *pgproto3.ErrorResponse:
expectedError = queryMessage
case *pgproto3.NoData:
case *pgproto3.ParseComplete:
case *pgproto3.ReadyForQuery:
break ParseLoop
default:
return nil, errors.Errorf("unable to determine what to do with %T", queryMessage)
}
}
var responseError *pgproto3.ErrorResponse
ParseResponseLoop:
for {
response, err := connection.Receive()
if err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.Query,
UnexpectedError: err.Error(),
})
continue ListenerLoop
}
response = DuplicateMessage(response).(pgproto3.BackendMessage)
switch response := response.(type) {
case *pgproto3.EmptyQueryResponse:
case *pgproto3.ErrorResponse:
responseError = response
case *pgproto3.NoData:
case *pgproto3.NoticeResponse:
case *pgproto3.ParseComplete:
case *pgproto3.ReadyForQuery:
break ParseResponseLoop
default:
return nil, errors.Errorf("unable to determine what to do with %T", message)
}
}
if err = connection.EmptyReceiveBuffer(); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.Query,
UnexpectedError: err.Error(),
})
continue MessageLoop
}
if expectedError == nil {
if responseError != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.Query,
UnexpectedError: responseError.Message,
})
continue MessageLoop
}
tracker.Success++
tracker.AddSuccess(ReplayTrackerItem{
Query: message.Query,
})
} else /* expectedError != nil */ {
if responseError == nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.Query,
ExpectedError: expectedError.Message,
})
continue MessageLoop
}
tracker.Success++
tracker.AddSuccess(ReplayTrackerItem{
Query: message.Query,
})
if expectedError.Message != responseError.Message {
tracker.PartialSuccess++
tracker.AddFailure(ReplayTrackerItem{
Query: message.Query,
UnexpectedError: responseError.Message,
ExpectedError: expectedError.Message,
})
}
}
case *pgproto3.Query:
if options.PrintQueries {
fmt.Println("QUERY: " + message.String)
}
if options.FailPSQL {
if strings.HasPrefix(message.String, "SELECT c2.relname, i.indisprimary, i.indisunique, i.indisclustered, i.indisvalid, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true),") {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: "set to automatically fail PSQL commands",
})
reader.SyncToNextQuery()
continue MessageLoop
}
}
for _, failQuery := range options.FailQueries {
if strings.Contains(message.String, failQuery) {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: "set to automatically fail due to catastrophic error (OOM, stack limit, etc.)",
})
reader.SyncToNextQuery()
continue MessageLoop
}
}
if err = connection.Send(message); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: err.Error(),
})
continue ListenerLoop
}
var expectedError *pgproto3.ErrorResponse
var expectedRowDesc *pgproto3.RowDescription
var expectedDataRows []*pgproto3.DataRow
var expectedCopyData []*pgproto3.CopyData
QueryLoop:
for {
switch queryMessage := reader.Next().(type) {
case *pgproto3.CommandComplete:
case *pgproto3.CopyData:
expectedCopyData = append(expectedCopyData, queryMessage)
case *pgproto3.CopyDone:
case *pgproto3.CopyInResponse:
case *pgproto3.CopyOutResponse:
case *pgproto3.DataRow:
expectedDataRows = append(expectedDataRows, queryMessage)
case *pgproto3.EmptyQueryResponse:
case *pgproto3.ErrorResponse:
expectedError = queryMessage
case *pgproto3.ReadyForQuery:
break QueryLoop
case *pgproto3.RowDescription:
expectedRowDesc = queryMessage
default:
return nil, errors.Errorf("unable to determine what to do with %T", queryMessage)
}
}
var responseError *pgproto3.ErrorResponse
var responseRowDesc *pgproto3.RowDescription
var responseDataRows []*pgproto3.DataRow
ResponseLoop:
for {
response, err := connection.Receive()
if err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: err.Error(),
})
continue ListenerLoop
}
response = DuplicateMessage(response).(pgproto3.BackendMessage)
switch response := response.(type) {
case *pgproto3.CommandComplete:
case *pgproto3.CopyInResponse:
for _, copyData := range expectedCopyData {
connection.Queue(copyData)
}
if err = connection.SendNoSync(&pgproto3.CopyDone{}); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: err.Error(),
})
continue ListenerLoop
}
case *pgproto3.DataRow:
responseDataRows = append(responseDataRows, response)
case *pgproto3.EmptyQueryResponse:
case *pgproto3.ErrorResponse:
responseError = response
case *pgproto3.NoticeResponse:
case *pgproto3.ReadyForQuery:
break ResponseLoop
case *pgproto3.RowDescription:
responseRowDesc = response
default:
return nil, errors.Errorf("unable to determine what to do with %T", message)
}
}
if err = connection.EmptyReceiveBuffer(); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: err.Error(),
})
continue MessageLoop
}
if expectedError == nil {
if responseError != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: responseError.Message,
})
continue MessageLoop
}
if expectedRowDesc == nil {
if responseRowDesc == nil {
tracker.Success++
tracker.AddSuccess(ReplayTrackerItem{
Query: message.String,
})
} else {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: "expected no rows but received rows",
})
}
continue MessageLoop
}
if responseRowDesc == nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: "expected rows but received none",
})
continue MessageLoop
}
if len(expectedRowDesc.Fields) != len(responseRowDesc.Fields) {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: fmt.Sprintf("expected column count %d but received %d",
len(expectedRowDesc.Fields), len(responseRowDesc.Fields)),
})
continue MessageLoop
}
var partialSuccesses []string
for i := range expectedRowDesc.Fields {
expectedName := string(expectedRowDesc.Fields[i].Name)
responseName := string(responseRowDesc.Fields[i].Name)
if expectedName != responseName {
partialSuccesses = append(partialSuccesses,
fmt.Sprintf("expected column with name `%s` but received `%s`", expectedName, responseName))
}
// TODO: determine if we should also check column types
}
if len(expectedDataRows) != len(responseDataRows) {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: fmt.Sprintf("expected row count %d but received %d",
len(expectedDataRows), len(responseDataRows)),
})
continue MessageLoop
}
if strings.Contains(strings.ToLower(message.String), "order by") {
// There's an ORDER BY, so we need to check based on the order
if err = CompareRowsOrdered(expectedRowDesc, responseRowDesc, expectedDataRows, responseDataRows); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: err.Error(),
})
continue MessageLoop
}
} else {
// There's no ORDER BY, so our native row order may differ from Postgres.
if err = CompareRowsUnordered(expectedRowDesc, responseRowDesc, expectedDataRows, responseDataRows); err != nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: err.Error(),
})
continue MessageLoop
}
}
tracker.Success++
tracker.AddSuccess(ReplayTrackerItem{
Query: message.String,
})
if len(partialSuccesses) > 0 {
tracker.PartialSuccess++
}
} else /* expectedError != nil */ {
if responseError == nil {
tracker.Failed++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
ExpectedError: expectedError.Message,
})
continue MessageLoop
}
tracker.Success++
tracker.AddSuccess(ReplayTrackerItem{
Query: message.String,
})
if expectedError.Message != responseError.Message {
tracker.PartialSuccess++
tracker.AddFailure(ReplayTrackerItem{
Query: message.String,
UnexpectedError: responseError.Message,
ExpectedError: expectedError.Message,
})
}
}
case *pgproto3.Terminate:
if err = connection.SendNoSync(message); err != nil {
return nil, err
}
break MessageLoop
default:
return nil, errors.Errorf("unable to determine what to do with %T", message)
}
}
connection.Close()
}
elapsed := time.Since(t)
fmt.Printf("-------------------- %s done in %fs --------------------", tracker.File, elapsed.Seconds())
return tracker, nil
}
@@ -0,0 +1,128 @@
// Copyright 2024 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 main
import (
"path/filepath"
"sort"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/doltgresql/utils"
)
// ReplayTracker tracks data for a Replay run.
type ReplayTracker struct {
File string
Success uint32
PartialSuccess uint32
Failed uint32
SuccessItems []ReplayTrackerItem
FailPartialItems []ReplayTrackerItem
}
// ReplayTrackerItem specifically tracks partial successes and failures for queries.
type ReplayTrackerItem struct {
Query string
PartialSuccess []string
UnexpectedError string
ExpectedError string
}
// NewReplayTracker returns a new *ReplayTracker.
func NewReplayTracker(file string) *ReplayTracker {
return &ReplayTracker{
File: strings.ReplaceAll(filepath.Base(file), ".results", ""),
Success: 0,
PartialSuccess: 0,
Failed: 0,
SuccessItems: nil,
FailPartialItems: nil,
}
}
// AddSuccess adds the given ReplayTrackerItem as a Success.
func (rt *ReplayTracker) AddSuccess(item ReplayTrackerItem) {
rt.SuccessItems = append(rt.SuccessItems, item)
}
// AddFailure adds the given ReplayTrackerItem as a Failure (or Partial Success).
func (rt *ReplayTracker) AddFailure(item ReplayTrackerItem) {
rt.FailPartialItems = append(rt.FailPartialItems, item)
}
// SerializeTrackers serializes the given trackers.
func SerializeTrackers(trackers ...*ReplayTracker) []byte {
sort.Slice(trackers, func(i, j int) bool {
return trackers[i].File < trackers[j].File
})
writer := utils.NewWriter(1048576)
writer.Uint32(2) // Version
writer.Uint32(uint32(len(trackers)))
for _, tracker := range trackers {
writer.String(tracker.File)
writer.Uint32(tracker.Success)
writer.Uint32(tracker.PartialSuccess)
writer.Uint32(tracker.Failed)
writer.Uint32(uint32(len(tracker.SuccessItems)))
for _, item := range tracker.SuccessItems {
writer.String(item.Query)
}
writer.Uint32(uint32(len(tracker.FailPartialItems)))
for _, item := range tracker.FailPartialItems {
writer.String(item.Query)
writer.StringSlice(item.PartialSuccess)
writer.String(item.UnexpectedError)
writer.String(item.ExpectedError)
}
}
return writer.Data()
}
// DeserializeTrackers deserializes the given data into a sorted list of trackers.
func DeserializeTrackers(data []byte) ([]*ReplayTracker, error) {
reader := utils.NewReader(data)
version := reader.Uint32()
if version != 2 {
return nil, errors.Errorf("version %d is not supported by this branch", version)
}
trackers := make([]*ReplayTracker, reader.Uint32())
for trackerIdx := 0; trackerIdx < len(trackers); trackerIdx++ {
trackers[trackerIdx] = &ReplayTracker{}
trackers[trackerIdx].File = reader.String()
trackers[trackerIdx].Success = reader.Uint32()
trackers[trackerIdx].PartialSuccess = reader.Uint32()
trackers[trackerIdx].Failed = reader.Uint32()
trackers[trackerIdx].SuccessItems = make([]ReplayTrackerItem, reader.Uint32())
for itemIdx := 0; itemIdx < len(trackers[trackerIdx].SuccessItems); itemIdx++ {
trackers[trackerIdx].SuccessItems[itemIdx].Query = reader.String()
}
trackers[trackerIdx].FailPartialItems = make([]ReplayTrackerItem, reader.Uint32())
for itemIdx := 0; itemIdx < len(trackers[trackerIdx].FailPartialItems); itemIdx++ {
trackers[trackerIdx].FailPartialItems[itemIdx].Query = reader.String()
trackers[trackerIdx].FailPartialItems[itemIdx].PartialSuccess = reader.StringSlice()
trackers[trackerIdx].FailPartialItems[itemIdx].UnexpectedError = reader.String()
trackers[trackerIdx].FailPartialItems[itemIdx].ExpectedError = reader.String()
}
}
sort.Slice(trackers, func(i, j int) bool {
return trackers[i].File < trackers[j].File
})
if !reader.IsEmpty() {
return trackers, errors.Errorf("additional data remaining after all trackers have been read")
}
return trackers, nil
}
@@ -0,0 +1,224 @@
// Copyright 2024 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 main
// AllTestResultFilesNames contains the names of all of the test result files that should be created.
var AllTestResultFilesNames = []string{
"results/test_setup.results",
"results/tablespace.results",
"results/boolean.results",
"results/char.results",
"results/name.results",
"results/varchar.results",
"results/text.results",
"results/int2.results",
"results/int4.results",
"results/int8.results",
"results/oid.results",
"results/float4.results",
"results/float8.results",
"results/bit.results",
"results/numeric.results",
"results/txid.results",
"results/uuid.results",
"results/enum.results",
"results/money.results",
"results/rangetypes.results",
"results/pg_lsn.results",
"results/regproc.results",
"results/strings.results",
"results/numerology.results",
"results/point.results",
"results/lseg.results",
"results/line.results",
"results/box.results",
"results/path.results",
"results/polygon.results",
"results/circle.results",
"results/date.results",
"results/time.results",
"results/timetz.results",
"results/timestamp.results",
"results/timestamptz.results",
"results/interval.results",
"results/inet.results",
"results/macaddr.results",
"results/macaddr8.results",
"results/multirangetypes.results",
"results/geometry.results",
"results/horology.results",
"results/tstypes.results",
"results/regex.results",
"results/type_sanity.results",
"results/opr_sanity.results",
"results/misc_sanity.results",
"results/comments.results",
"results/expressions.results",
"results/unicode.results",
"results/xid.results",
"results/mvcc.results",
"results/copy.results",
"results/copyselect.results",
"results/copydml.results",
"results/insert.results",
"results/insert_conflict.results",
"results/create_misc.results",
"results/create_operator.results",
"results/create_procedure.results",
"results/create_table.results",
"results/create_type.results",
"results/create_schema.results",
"results/create_index.results",
"results/create_index_spgist.results",
"results/create_view.results",
"results/index_including.results",
"results/index_including_gist.results",
"results/create_aggregate.results",
"results/create_function_sql.results",
"results/create_cast.results",
"results/constraints.results",
"results/triggers.results",
"results/select.results",
"results/inherit.results",
"results/typed_table.results",
"results/vacuum.results",
"results/drop_if_exists.results",
"results/updatable_views.results",
"results/roleattributes.results",
"results/create_am.results",
"results/hash_func.results",
"results/errors.results",
"results/infinite_recurse.results",
"results/sanity_check.results",
"results/select_into.results",
"results/select_distinct.results",
"results/select_distinct_on.results",
"results/select_implicit.results",
"results/select_having.results",
"results/subselect.results",
"results/union.results",
"results/case.results",
"results/join.results",
"results/aggregates.results",
"results/transactions.results",
"results/random.results",
"results/portals.results",
"results/arrays.results",
"results/btree_index.results",
"results/hash_index.results",
"results/update.results",
"results/delete.results",
"results/namespace.results",
"results/brin.results",
"results/gin.results",
"results/gist.results",
"results/spgist.results",
"results/init_privs.results",
"results/security_label.results",
"results/collate.results",
"results/matview.results",
"results/lock.results",
"results/replica_identity.results",
"results/rowsecurity.results",
"results/object_address.results",
"results/tablesample.results",
"results/groupingsets.results",
"results/drop_operator.results",
"results/password.results",
"results/identity.results",
"results/generated.results",
"results/join_hash.results",
"results/brin_bloom.results",
"results/brin_multi.results",
"results/create_table_like.results",
"results/alter_generic.results",
"results/alter_operator.results",
"results/misc.results",
"results/async.results",
"results/dbsize.results",
"results/merge.results",
"results/misc_functions.results",
"results/sysviews.results",
"results/tsrf.results",
"results/tid.results",
"results/tidscan.results",
"results/tidrangescan.results",
"results/collate.icu.utf8.results",
"results/incremental_sort.results",
"results/create_role.results",
"results/rules.results",
"results/psql.results",
"results/psql_crosstab.results",
"results/amutils.results",
"results/stats_ext.results",
"results/select_parallel.results",
"results/write_parallel.results",
"results/vacuum_parallel.results",
"results/publication.results",
"results/subscription.results",
"results/select_views.results",
"results/portals_p2.results",
"results/foreign_key.results",
"results/cluster.results",
"results/dependency.results",
"results/guc.results",
"results/bitmapops.results",
"results/combocid.results",
"results/tsearch.results",
"results/tsdicts.results",
"results/window.results",
"results/xmlmap.results",
"results/functional_deps.results",
"results/advisory_lock.results",
"results/indirect_toast.results",
"results/equivclass.results",
"results/json.results",
"results/jsonb.results",
"results/json_encoding.results",
"results/jsonpath.results",
"results/jsonpath_encoding.results",
"results/jsonb_jsonpath.results",
"results/plancache.results",
"results/limit.results",
"results/plpgsql.results",
"results/copy2.results",
"results/domain.results",
"results/rangefuncs.results",
"results/prepare.results",
"results/conversion.results",
"results/truncate.results",
"results/alter_table.results",
"results/sequence.results",
"results/polymorphism.results",
"results/rowtypes.results",
"results/returning.results",
"results/largeobject.results",
"results/with.results",
"results/xml.results",
"results/partition_join.results",
"results/partition_prune.results",
"results/reloptions.results",
"results/hash_part.results",
"results/indexing.results",
"results/partition_aggregate.results",
"results/partition_info.results",
"results/tuplesort.results",
"results/explain.results",
"results/compression.results",
"results/memoize.results",
"results/event_trigger.results",
"results/oidjoins.results",
"results/fast_default.results",
}
@@ -0,0 +1,58 @@
// Copyright 2024 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 main
import (
"path/filepath"
"regexp"
"strings"
"github.com/cockroachdb/errors"
"github.com/jackc/pgx/v5/pgproto3"
)
var copyFrom, _ = regexp.Compile(`COPY\s+\S+\s+FROM\s+'([a-zA-Z0-9_\/\\%#@!~+=:.-]+)'`)
// RewriteCopyToLocal rewrites a COPY ... FROM query so that it points to the data file that is located in our data
// directory.
func RewriteCopyToLocal(query *pgproto3.Query) (*pgproto3.Query, error) {
matches := copyFrom.FindStringSubmatch(query.String)
if len(matches) != 2 {
return query, nil
}
fileName := filepath.Base(matches[1])
relativeFileName := "data/" + fileName
if ok, err := regressionFolder.Exists(relativeFileName); err != nil {
return query, err
} else if !ok {
return query, errors.Errorf("file does not exist: '%s'", regressionFolder.GetAbsolutePath(relativeFileName))
}
return &pgproto3.Query{
String: strings.ReplaceAll(query.String, matches[1], regressionFolder.GetAbsolutePath(relativeFileName)),
}, nil
}
// RewriteCopyToFileOnly rewrites a COPY ... FROM query so that it only contains the file name. This is mainly used so
// that serialization does not any local information from the user's machine.
func RewriteCopyToFileOnly(query *pgproto3.Query) *pgproto3.Query {
matches := copyFrom.FindStringSubmatch(query.String)
if len(matches) != 2 {
return query
}
return &pgproto3.Query{
String: strings.ReplaceAll(query.String, matches[1], filepath.Base(matches[1])),
}
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2024 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 main
import (
"fmt"
"os"
"testing"
"github.com/dolthub/doltgresql/server"
"github.com/stretchr/testify/require"
)
func TestRegressionTests(t *testing.T) {
// We'll only run this on GitHub Actions, so set this environment variable to run locally
if _, ok := os.LookupEnv("REGRESSION_TESTING"); !ok {
t.Skip()
}
server.EnableAuthentication = false // We have to disable authentication, since we can't replay the messages due to nonces
controller, port, err := CreateDoltgresServer()
require.NoError(t, err)
defer func() {
controller.Stop()
err = controller.WaitForStop()
require.NoError(t, err)
}()
trackers := make([]*ReplayTracker, 0, len(AllTestResultFilesNames))
for _, fileName := range AllTestResultFilesNames {
messages, err := regressionFolder.ReadMessages(fileName)
require.NoError(t, err)
tracker, err := Replay(ReplayOptions{
File: fileName,
Port: port,
Messages: messages,
PrintQueries: false,
FailPSQL: true,
FailQueries: queriesToSkip,
})
require.NoError(t, err)
trackers = append(trackers, tracker)
}
fmt.Printf("Finished, writing output to `%s`\n", regressionFolder.GetAbsolutePath("out/results.trackers"))
err = regressionFolder.WriteReplayTrackers("out/results.trackers", trackers, 0644)
require.NoError(t, err)
}
var queriesToSkip = []string{
`CREATE VIEW lock_view7 AS SELECT * from lock_view2;`,
`create index testtable_apple_index on testtable_apple(logdate);`,
`create index testtable_orange_index on testtable_orange(logdate);`,
`create table child_0_10 partition of parent_tab
for values from (0) to (10);`,
`create table child_10_20 partition of parent_tab
for values from (10) to (20);`,
`create table child_20_30 partition of parent_tab
for values from (20) to (30);`,
`create table child_30_35 partition of child_30_40
for values from (30) to (35);`,
`create table child_35_40 partition of child_30_40
for values from (35) to (40);`,
`select count(*)
from
(select t3.tenthous as x1, coalesce(t1.stringu1, t2.stringu1) as x2
from tenk1 t1
left join tenk1 t2 on t1.unique1 = t2.unique1
join tenk1 t3 on t1.unique2 = t3.unique2) ss,
tenk1 t4,
tenk1 t5
where t4.thousand = t5.unique1 and ss.x1 = t4.tenthous and ss.x2 = t5.stringu1;`,
`select count(*) from
(select * from tenk1 x order by x.thousand, x.twothousand, x.fivethous) x
left join
(select * from tenk1 y order by y.unique2) y
on x.thousand = y.unique2 and x.twothousand = y.hundred and x.fivethous = y.unique2;`,
`select count(*) from tenk1 a, tenk1 b
where a.hundred = b.thousand and (b.fivethous % 10) < 10;`,
`select a.unique2, a.ten, b.tenthous, b.unique2, b.hundred
from tenk1 a left join tenk1 b on a.unique2 = b.tenthous
where a.unique1 = 42 and
((b.unique2 is null and a.ten = 2) or b.hundred = 3);`,
`select
(select max((select i.unique2 from tenk1 i where i.unique1 = o.unique1)))
from tenk1 o;`,
`SELECT pg_class.relname FROM pg_index, pg_class, pg_class AS pg_class_2
WHERE pg_class.oid=indexrelid
AND indrelid=pg_class_2.oid
AND pg_class_2.relname = 'clstr_tst'
AND indisclustered;`,
`SELECT 1 FROM pg_catalog.pg_constraint WHERE conrelid = i.indrelid AND conindid = i.indexrelid`,
`SELECT generate_series(1, generate_series(1, 3))`,
`SELECT generate_series(generate_series(1,3), generate_series(2, 4));`,
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2024 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 main
import (
"net"
"github.com/cockroachdb/errors"
"github.com/jackc/pgx/v5/pgproto3"
)
// handleStartup handles the startup messages.
func handleStartup(clientConnBackend *pgproto3.Backend, postgresConnFrontend *pgproto3.Frontend, clientConn net.Conn) error {
StartupLoop:
for {
startupMessage, err := clientConnBackend.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:
addMessage(startupMessage)
postgresConnFrontend.Send(startupMessage)
if err = postgresConnFrontend.Flush(); err != nil {
return err
}
response, err := postgresConnFrontend.Receive()
if err != nil {
return err
}
if err = setAuthType(clientConnBackend, response); err != nil {
return err
}
addMessage(response)
clientConnBackend.Send(response)
if err = clientConnBackend.Flush(); err != nil {
return err
}
break StartupLoop
default:
return errors.Errorf("Unexpected Startup Message: %v", startupMessage)
}
}
return nil
}