951 lines
32 KiB
Go
951 lines
32 KiB
Go
// Copyright 2023 Dolthub, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package _go
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
goerrors "errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cockroachdb/apd/v3"
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
|
|
"github.com/dolthub/dolt/go/libraries/doltcore/env"
|
|
"github.com/dolthub/dolt/go/libraries/utils/filesys"
|
|
"github.com/dolthub/dolt/go/libraries/utils/svcs"
|
|
"github.com/dolthub/go-mysql-server/sql"
|
|
gmstypes "github.com/dolthub/go-mysql-server/sql/types"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/dolthub/doltgresql/core/id"
|
|
"github.com/dolthub/doltgresql/postgres/parser/duration"
|
|
pgtree "github.com/dolthub/doltgresql/postgres/parser/sem/tree"
|
|
"github.com/dolthub/doltgresql/postgres/parser/timeofday"
|
|
"github.com/dolthub/doltgresql/postgres/parser/uuid"
|
|
dserver "github.com/dolthub/doltgresql/server"
|
|
"github.com/dolthub/doltgresql/server/auth"
|
|
"github.com/dolthub/doltgresql/server/functions"
|
|
"github.com/dolthub/doltgresql/server/types"
|
|
"github.com/dolthub/doltgresql/servercfg"
|
|
"github.com/dolthub/doltgresql/servercfg/cfgdetails"
|
|
)
|
|
|
|
// runOnPostgres is a debug setting to redirect the test framework to a local running postgres server,
|
|
// rather than starting a doltgres server.
|
|
const runOnPostgres = false
|
|
|
|
// serverHost is the host of the local Doltgres server used for testing; set to IPv4 loopback address.
|
|
var serverHost = "127.0.0.1"
|
|
|
|
// ScriptTest defines a consistent structure for testing queries.
|
|
type ScriptTest struct {
|
|
// Name of the script.
|
|
Name string
|
|
// The database to create and use. If not provided, then it defaults to "postgres".
|
|
Database string
|
|
// The SQL statements to execute as setup, in order. Results are not checked, but statements must not error.
|
|
SetUpScript []string
|
|
// The set of assertions to make after setup, in order
|
|
Assertions []ScriptTestAssertion
|
|
// When using RunScripts, setting this on one (or more) tests causes RunScripts to ignore all tests that have this
|
|
// set to false (which is the default value). This allows a developer to easily "focus" on a specific test without
|
|
// having to comment out other tests, pull it into a different function, etc. In addition, CI ensures that this is
|
|
// false before passing, meaning this prevents the commented-out situation where the developer forgets to uncomment
|
|
// their code.
|
|
Focus bool
|
|
// Skip is used to completely skip a test including setup
|
|
Skip bool
|
|
// UseLocalFileSystem determines if the test should use the local filesystem
|
|
UseLocalFileSystem bool
|
|
}
|
|
|
|
// ExpectedNotice specifies what notices are expected during a script test assertion.
|
|
type ExpectedNotice struct {
|
|
Severity string
|
|
Message string
|
|
}
|
|
|
|
// ScriptTestAssertion are the assertions upon which the script executes its main "testing" logic.
|
|
type ScriptTestAssertion struct {
|
|
Query string
|
|
Expected []sql.Row // Expected or ExpectedRaw should be used, but not both at the same time
|
|
ExpectedRaw [][][]byte // ExpectedRaw or Expected should be used, but not both at the same time
|
|
ExpectedErr string
|
|
ExpectedNotices []ExpectedNotice
|
|
Focus bool
|
|
|
|
BindVars []any
|
|
|
|
// SkipResultsCheck is used to skip assertions on the expected rows returned from a query. For now, this is
|
|
// included as some messages do not have a full logical implementation. Skipping the results check allows us to
|
|
// force the test client to not send of those messages.
|
|
SkipResultsCheck bool
|
|
|
|
// Skip is used to completely skip a test, not execute its query at all, and record it as a skipped test
|
|
// in the test suite results.
|
|
Skip bool
|
|
|
|
// Username specifies the user's name to use for the command. This creates a new connection, using the given name.
|
|
// By default (when the string is empty), the `postgres` superuser account is used. Any consecutive queries that
|
|
// have the same username and password will reuse the same connection. The `postgres` superuser account will always
|
|
// reuse the same connection. Do note that specifying the `postgres` account manually will create a connection
|
|
// that is different from the primary one.
|
|
Username string
|
|
// Password specifies the password that will be used alongside the given username. This field is essentially ignored
|
|
// when no username is given. If a username is given and the password is empty, then it is assumed that the password
|
|
// is the empty string.
|
|
Password string
|
|
|
|
// ExpectedTag is used to check the command tag returned from the server.
|
|
// This is checked only if no Expected is defined
|
|
ExpectedTag string
|
|
|
|
// ExpectedColNames are used to check the column names returned from the server.
|
|
ExpectedColNames []string
|
|
|
|
// ExpectedColTypes are used to check the column types returned from the server.
|
|
ExpectedColTypes []id.Type
|
|
|
|
// CopyFromSTDIN is used to test the COPY FROM STDIN command.
|
|
CopyFromStdInFile string
|
|
}
|
|
|
|
// EmptyCommandTag is special command tag placeholder to check for the empty string
|
|
const EmptyCommandTag = "EMPTY_COMMAND_TAG"
|
|
|
|
// Connection contains the default and current connections.
|
|
type Connection struct {
|
|
Default *pgx.Conn
|
|
Current *pgx.Conn
|
|
Username string
|
|
Password string
|
|
}
|
|
|
|
// receivedNotices tracks the NOTICE messages received over the connection to the Doltgres server, so that tests
|
|
// can assert what notices are expected to be sent to the client.
|
|
var receivedNotices []*pgconn.Notice
|
|
|
|
// RunScript runs the given script.
|
|
func RunScript(t *testing.T, script ScriptTest, normalizeRows bool) {
|
|
scriptDatabase := script.Database
|
|
if len(scriptDatabase) == 0 {
|
|
scriptDatabase = "postgres"
|
|
}
|
|
|
|
var ctx context.Context
|
|
var conn *Connection
|
|
|
|
if runOnPostgres {
|
|
ctx = context.Background()
|
|
pgxConn, err := pgx.Connect(ctx, fmt.Sprintf("postgres://postgres:password@127.0.0.1:%d/%s?sslmode=disable", 5432, scriptDatabase))
|
|
require.NoError(t, err)
|
|
conn = &Connection{
|
|
Default: pgxConn,
|
|
Current: pgxConn,
|
|
}
|
|
require.NoError(t, pgxConn.Ping(ctx))
|
|
defer func() {
|
|
conn.Close(ctx)
|
|
}()
|
|
} else {
|
|
var controller *svcs.Controller
|
|
if script.UseLocalFileSystem {
|
|
port, err := sql.GetEmptyPort()
|
|
require.NoError(t, err)
|
|
ctx, conn, controller = CreateServerLocalWithPort(t, scriptDatabase, port)
|
|
} else {
|
|
ctx, conn, controller = CreateServer(t, scriptDatabase)
|
|
}
|
|
defer func() {
|
|
conn.Close(ctx)
|
|
controller.Stop()
|
|
err := controller.WaitForStop()
|
|
require.NoError(t, err)
|
|
}()
|
|
}
|
|
|
|
t.Run(script.Name, func(t *testing.T) {
|
|
runScript(t, ctx, script, conn, normalizeRows)
|
|
})
|
|
}
|
|
|
|
// runScript runs the script given on the postgres connection provided
|
|
func runScript(t *testing.T, ctx context.Context, script ScriptTest, conn *Connection, normalizeRows bool) {
|
|
if script.Skip {
|
|
t.Skip("Skip has been set in the script")
|
|
}
|
|
|
|
// Run the setup
|
|
for _, query := range script.SetUpScript {
|
|
_, err := conn.Exec(ctx, query)
|
|
require.NoError(t, err, "error running setup query: %s", query)
|
|
}
|
|
|
|
assertions := script.Assertions
|
|
// First, we'll run through the scripts to check for the Focus variable. If it's true, then append it to the new slice.
|
|
focusAssertions := make([]ScriptTestAssertion, 0, len(script.Assertions))
|
|
for _, assertion := range script.Assertions {
|
|
if assertion.Focus {
|
|
// If this is running in GitHub Actions, then we'll panic, because someone forgot to disable it before committing
|
|
if _, ok := os.LookupEnv("GITHUB_ACTION"); ok {
|
|
panic(fmt.Sprintf("The assertion `%s` has Focus set to `true`. GitHub Actions requires that "+
|
|
"all tests are run, which Focus circumvents, leading to this error. Please disable Focus on "+
|
|
"all tests.", assertion.Query))
|
|
}
|
|
focusAssertions = append(focusAssertions, assertion)
|
|
}
|
|
}
|
|
// If we have assertions with Focus set, then we replace the normal script slice with the new slice.
|
|
if len(focusAssertions) > 0 {
|
|
assertions = focusAssertions
|
|
}
|
|
|
|
// Run the assertions
|
|
for _, assertion := range assertions {
|
|
t.Run(assertion.Query, func(t *testing.T) {
|
|
if assertion.Skip {
|
|
t.Skip("Skip has been set in the assertion")
|
|
}
|
|
|
|
// Clear out any previously received notices
|
|
receivedNotices = nil
|
|
|
|
// Use the provided username and password to create a new connection (if a username has been specified).
|
|
// This will automatically handle connection reuse, using the default connection is no user is specified, etc.
|
|
if err := conn.Connect(ctx, assertion.Username, assertion.Password); err != nil {
|
|
if assertion.ExpectedErr != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), assertion.ExpectedErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
return
|
|
}
|
|
// If we're skipping the results check, then we call Execute, as it uses a simplified message model.
|
|
if assertion.CopyFromStdInFile != "" {
|
|
copyFromStdin(t, conn.Current, assertion.Query, assertion.CopyFromStdInFile)
|
|
} else if assertion.SkipResultsCheck || assertion.ExpectedErr != "" {
|
|
_, err := conn.Exec(ctx, assertion.Query, assertion.BindVars...)
|
|
if assertion.ExpectedErr != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), assertion.ExpectedErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
} else if assertion.ExpectedTag != "" {
|
|
commandTag, err := conn.Exec(ctx, assertion.Query)
|
|
require.NoError(t, err)
|
|
tag := assertion.ExpectedTag
|
|
if tag == EmptyCommandTag {
|
|
tag = ""
|
|
}
|
|
|
|
assert.Equal(t, tag, commandTag.String())
|
|
} else {
|
|
rows, err := conn.Query(ctx, assertion.Query, assertion.BindVars...)
|
|
require.NoError(t, err)
|
|
readRows, readRawRows, err := ReadRows(rows, normalizeRows)
|
|
require.NoError(t, err)
|
|
|
|
if assertion.ExpectedColNames != nil {
|
|
fields := rows.FieldDescriptions()
|
|
if assert.Len(t, fields, len(assertion.ExpectedColNames), "expected length of columns") {
|
|
for i, col := range assertion.ExpectedColNames {
|
|
assert.Equal(t, col, fields[i].Name)
|
|
}
|
|
}
|
|
}
|
|
if assertion.ExpectedColTypes != nil {
|
|
fields := rows.FieldDescriptions()
|
|
if assert.Len(t, fields, len(assertion.ExpectedColTypes),
|
|
"columns returned and types expected are not the same length") {
|
|
for i, colId := range assertion.ExpectedColTypes {
|
|
assert.Equal(t, id.Cache().ToOID(colId.AsId()), fields[i].DataTypeOID,
|
|
`"%s" expected type "%s" but received "%s"`, fields[i].Name,
|
|
colId.TypeName(), id.Type(id.Cache().ToInternal(fields[i].DataTypeOID)).TypeName())
|
|
}
|
|
}
|
|
}
|
|
|
|
// not an exact match but works well enough for our tests
|
|
orderBy := strings.Contains(strings.ToLower(assertion.Query), "order by")
|
|
|
|
if assertion.ExpectedRaw != nil {
|
|
if orderBy {
|
|
assert.Equal(t, assertion.ExpectedRaw, readRawRows, "wrong result for query %s", assertion.Query)
|
|
} else {
|
|
assert.ElementsMatch(t, assertion.ExpectedRaw, readRawRows, "wrong result for query %s", assertion.Query)
|
|
}
|
|
} else {
|
|
if normalizeRows {
|
|
if orderBy {
|
|
assert.Equal(t, NormalizeExpectedRow(rows.FieldDescriptions(), assertion.Expected), readRows, "wrong result for query %s", assertion.Query)
|
|
} else {
|
|
assert.ElementsMatch(t, NormalizeExpectedRow(rows.FieldDescriptions(), assertion.Expected), readRows, "wrong result for query %s", assertion.Query)
|
|
}
|
|
} else {
|
|
if orderBy {
|
|
assert.Equal(t, assertion.Expected, readRows, "wrong result for query %s", assertion.Query)
|
|
} else {
|
|
assert.ElementsMatch(t, assertion.Expected, readRows, "wrong result for query %s", assertion.Query)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(assertion.ExpectedNotices) > 0 {
|
|
if len(assertion.ExpectedNotices) == len(receivedNotices) {
|
|
for i, notice := range receivedNotices {
|
|
assert.Equal(t, assertion.ExpectedNotices[i].Severity, notice.Severity)
|
|
assert.Equal(t, assertion.ExpectedNotices[i].Message, notice.Message)
|
|
}
|
|
} else {
|
|
if len(receivedNotices) == 0 {
|
|
receivedNotices = []*pgconn.Notice{}
|
|
}
|
|
assert.Fail(t, "Received notices do not match expected notices",
|
|
"Expected %d notices, but received %d. Expected: %v, Received: %v",
|
|
len(assertion.ExpectedNotices), len(receivedNotices), assertion.ExpectedNotices, receivedNotices)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func copyFromStdin(t *testing.T, conn *pgx.Conn, query string, filename string) {
|
|
filePath := filepath.Join("testdata", filename)
|
|
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to open file: %v", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
reader := bufio.NewReader(file)
|
|
_, err = conn.PgConn().CopyFrom(context.Background(), reader, query)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// RunScripts runs the given collection of scripts. This normalizes all rows before comparing them.
|
|
func RunScripts(t *testing.T, scripts []ScriptTest) {
|
|
runScripts(t, scripts, true)
|
|
}
|
|
|
|
// RunScriptsWithoutNormalization runs the given collection of scripts, without normalizing any rows.
|
|
func RunScriptsWithoutNormalization(t *testing.T, scripts []ScriptTest) {
|
|
runScripts(t, scripts, false)
|
|
}
|
|
|
|
// runScripts is the implementation of both RunScripts and RunScriptsWithoutNormalization.
|
|
func runScripts(t *testing.T, scripts []ScriptTest, normalizeRows bool) {
|
|
// First, we'll run through the scripts to check for the Focus variable. If it's true, then append it to the new slice.
|
|
focusScripts := make([]ScriptTest, 0, len(scripts))
|
|
for _, script := range scripts {
|
|
if script.Focus {
|
|
// If this is running in GitHub Actions, then we'll panic, because someone forgot to disable it before committing
|
|
if _, ok := os.LookupEnv("GITHUB_ACTION"); ok {
|
|
panic(fmt.Sprintf("The script `%s` has Focus set to `true`. GitHub Actions requires that "+
|
|
"all tests are run, which Focus circumvents, leading to this error. Please disable Focus on "+
|
|
"all tests.", script.Name))
|
|
}
|
|
focusScripts = append(focusScripts, script)
|
|
}
|
|
}
|
|
// If we have scripts with Focus set, then we replace the normal script slice with the new slice.
|
|
if len(focusScripts) > 0 {
|
|
scripts = focusScripts
|
|
}
|
|
|
|
for _, script := range scripts {
|
|
RunScript(t, script, normalizeRows)
|
|
}
|
|
}
|
|
|
|
func ptr[T any](val T) *T {
|
|
return &val
|
|
}
|
|
|
|
var testServerLogLevel = "warn"
|
|
|
|
func init() {
|
|
if logLevel, ok := os.LookupEnv("TEST_SERVER_LOG_LEVEL"); ok {
|
|
testServerLogLevel = logLevel
|
|
}
|
|
}
|
|
|
|
// CreateServer creates a server with the given database, returning a connection to the server. The server will close
|
|
// when the connection is closed (or loses its connection to the server). The accompanying [svcs.Controller] may be used
|
|
// to wait until the server has closed.
|
|
func CreateServer(t *testing.T, database string) (context.Context, *Connection, *svcs.Controller) {
|
|
port, err := sql.GetEmptyPort()
|
|
require.NoError(t, err)
|
|
return CreateServerWithPort(t, database, port)
|
|
}
|
|
|
|
// CreateServerWithPort creates a server with the given database and port, returning a connection to the server. The server will close
|
|
// when the connection is closed (or loses its connection to the server). The accompanying [svcs.Controller] may be used
|
|
// to wait until the server has closed.
|
|
func CreateServerWithPort(t *testing.T, database string, port int) (context.Context, *Connection, *svcs.Controller) {
|
|
require.NotEmpty(t, database)
|
|
controller, err := dserver.RunInMemory(&servercfg.DoltgresConfig{
|
|
DoltgresConfig: cfgdetails.DoltgresConfig{
|
|
ListenerConfig: &cfgdetails.DoltgresListenerConfig{
|
|
PortNumber: &port,
|
|
HostStr: &serverHost,
|
|
},
|
|
LogLevelStr: &testServerLogLevel,
|
|
},
|
|
}, dserver.NewListener)
|
|
require.NoError(t, err)
|
|
auth.ClearDatabase()
|
|
fmt.Printf("port is %d\n", port)
|
|
|
|
ctx := context.Background()
|
|
connection := newTestDatabaseConnection(t, ctx, database, serverHost, port)
|
|
return ctx, connection, controller
|
|
}
|
|
|
|
// CreateServerLocalWithPort creates a server using the local file system at [os.TempDir]. A Connection is returned to
|
|
// |database| at 127.0.0.1:|port|. The server will close when the connection is closed or lost. The returned
|
|
// [svcs.Controller] may be used to wait for the server to stop.
|
|
func CreateServerLocalWithPort(t *testing.T, database string, port int) (context.Context, *Connection, *svcs.Controller) {
|
|
// We avoid using [T.TempDir] because it results in a file lock conflict on Windows. [T.TempDir] registers a
|
|
// [T.Cleanup] function that runs without checking the [svcs.Controller] and it cannot be overwritten.
|
|
// TODO(elianddb): Setup an optional [T.Cleanup] function for the temporary directory. Our default setup for now is
|
|
// preferable for debugging the database after a failure.
|
|
// t.Name() contains "/" for subtests, which os.MkdirTemp rejects as a path separator in the pattern.
|
|
safeName := strings.ReplaceAll(t.Name(), "/", "_")
|
|
dbDir, err := os.MkdirTemp(os.TempDir(), safeName)
|
|
require.NoError(t, err)
|
|
fileSys, err := filesys.LocalFilesysWithWorkingDir(dbDir)
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
doltEnv := env.Load(ctx, env.GetCurrentUserHomeDir, fileSys, doltdb.LocalDirDoltDB, dserver.Version)
|
|
|
|
controller, err := dserver.RunOnDisk(ctx, &servercfg.DoltgresConfig{
|
|
DoltgresConfig: cfgdetails.DoltgresConfig{
|
|
ListenerConfig: &cfgdetails.DoltgresListenerConfig{
|
|
PortNumber: &port,
|
|
HostStr: &serverHost,
|
|
},
|
|
LogLevelStr: &testServerLogLevel,
|
|
},
|
|
}, doltEnv)
|
|
require.NoError(t, err)
|
|
auth.ClearDatabase()
|
|
fmt.Printf("port is %d\n", port)
|
|
|
|
connection := newTestDatabaseConnection(t, ctx, database, serverHost, port)
|
|
return ctx, connection, controller
|
|
}
|
|
|
|
// newTestDatabaseConnection returns a Connection to the test |database| at |host|:|port|. If the |database| provided
|
|
// does not exist, it will be automatically created.
|
|
func newTestDatabaseConnection(t *testing.T, ctx context.Context, database, host string, port int) *Connection {
|
|
const connectionUrlFmt = "postgres://postgres:password@%s:%d/%s?DateStyle=ISO%%2C%%20MDY"
|
|
func() {
|
|
var conn *pgx.Conn
|
|
var err error
|
|
// Connections can happen before the server has a chance to grab the port so we retry.
|
|
for range 3 {
|
|
conn, err = pgx.Connect(ctx, fmt.Sprintf(connectionUrlFmt, host, port, ""))
|
|
if err == nil {
|
|
break
|
|
}
|
|
|
|
time.Sleep(time.Second)
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
_, err = conn.Exec(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", database))
|
|
require.NoError(t, err)
|
|
|
|
defer require.NoError(t, conn.Close(ctx))
|
|
}()
|
|
|
|
config, err := pgx.ParseConfig(fmt.Sprintf(connectionUrlFmt, host, port, database))
|
|
require.NoError(t, err)
|
|
config.OnNotice = func(conn *pgconn.PgConn, notice *pgconn.Notice) {
|
|
receivedNotices = append(receivedNotices, notice)
|
|
}
|
|
// pgx v5.9.1+ skips Describe(portal) on statement-cache hits, so stale field descriptions
|
|
// from before a DDL change (e.g. DROP COLUMN) cause a field-count/value-count mismatch and
|
|
// panic in rows.Values(). DescribeExec re-describes every query, keeping schema info fresh.
|
|
// It also preserves binary wire format (needed for record type decoding).
|
|
config.DefaultQueryExecMode = pgx.QueryExecModeDescribeExec
|
|
|
|
conn, err := pgx.ConnectConfig(ctx, config)
|
|
require.NoError(t, err)
|
|
// Ping tests that Doltgres can handle empty queries, and makes sure connection is alive.
|
|
require.NoError(t, conn.Ping(ctx))
|
|
return &Connection{
|
|
Default: conn,
|
|
Current: conn,
|
|
}
|
|
}
|
|
|
|
// ReadRows reads all of the given rows into a slice, then closes the rows. If `normalizeRows` is true, then the rows
|
|
// will be normalized such that all integers are int64, etc. Normalization does not affect the raw returned bytes.
|
|
func ReadRows(rows pgx.Rows, normalizeRows bool) (readRows []sql.Row, readRawRows [][][]byte, err error) {
|
|
defer func() {
|
|
err = goerrors.Join(err, rows.Err())
|
|
}()
|
|
var slices []sql.Row
|
|
var rawSlices [][][]byte
|
|
for rows.Next() {
|
|
var rawSlice [][]byte
|
|
for _, rawValue := range rows.RawValues() {
|
|
rawSlice = append(rawSlice, append([]byte{}, rawValue...))
|
|
}
|
|
rawSlices = append(rawSlices, rawSlice)
|
|
row, err := rows.Values()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
slices = append(slices, row)
|
|
}
|
|
return NormalizeRows(rows.FieldDescriptions(), slices, normalizeRows), rawSlices, nil
|
|
}
|
|
|
|
// NormalizeRows normalizes each value's type within each row, as the tests only want to compare values. Returns a new
|
|
// set of rows in the same order.
|
|
func NormalizeRows(fds []pgconn.FieldDescription, rows []sql.Row, normalize bool) []sql.Row {
|
|
newRows := make([]sql.Row, len(rows))
|
|
for i := range rows {
|
|
newRows[i] = NormalizeRow(fds, rows[i], normalize)
|
|
}
|
|
return newRows
|
|
}
|
|
|
|
// NormalizeRow normalizes each value's type, as the tests only want to compare values.
|
|
// Returns a new row.
|
|
func NormalizeRow(fds []pgconn.FieldDescription, row sql.Row, normalize bool) sql.Row {
|
|
if len(row) == 0 {
|
|
return nil
|
|
}
|
|
newRow := make(sql.Row, len(row))
|
|
for i := range row {
|
|
dt, ok := types.IDToBuiltInDoltgresType[id.Type(id.Cache().ToInternal(fds[i].DataTypeOID))]
|
|
if !ok {
|
|
// try using text type
|
|
dt = types.Text
|
|
}
|
|
newRow[i] = NormalizeValToString(dt, row[i])
|
|
if normalize {
|
|
newRow[i] = NormalizeIntsAndFloats(newRow[i])
|
|
}
|
|
}
|
|
return newRow
|
|
}
|
|
|
|
// NormalizeExpectedRow normalizes each value's type, as the tests only want to compare values. Returns a new row.
|
|
func NormalizeExpectedRow(fds []pgconn.FieldDescription, rows []sql.Row) []sql.Row {
|
|
newRows := make([]sql.Row, len(rows))
|
|
for ri, row := range rows {
|
|
if len(row) == 0 {
|
|
newRows[ri] = nil
|
|
} else if len(row) != len(fds) {
|
|
// Return if the expected row count does not match the field description count, we'll error elsewhere
|
|
return rows
|
|
} else {
|
|
newRow := make(sql.Row, len(row))
|
|
for i := range row {
|
|
dt, ok := types.IDToBuiltInDoltgresType[id.Type(id.Cache().ToInternal(fds[i].DataTypeOID))]
|
|
if !ok {
|
|
// try using text type
|
|
dt = types.Text
|
|
}
|
|
if dt.ID == types.Json.ID && row[i] != nil {
|
|
newRow[i] = UnmarshalAndMarshalJsonString(row[i].(string))
|
|
} else if dt.IsArrayType() && dt.ArrayBaseType().ID == types.Json.ID {
|
|
v, err := dt.IoInput(nil, row[i].(string))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
arr := v.([]any)
|
|
newArr := make([]any, len(arr))
|
|
for j, el := range arr {
|
|
switch e := el.(type) {
|
|
case string:
|
|
newArr[j] = UnmarshalAndMarshalJsonString(e)
|
|
case sql.JSONWrapper:
|
|
iface, err := e.ToInterface(context.Background())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
b, err := json.Marshal(iface)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
newArr[j] = string(b)
|
|
default:
|
|
newArr[j] = el
|
|
}
|
|
}
|
|
ret, err := dt.FormatValue(newArr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
newRow[i] = ret
|
|
} else if dt.ID == types.Date.ID {
|
|
newRow[i] = row[i]
|
|
if row[i] != nil {
|
|
if t, _, err := pgtree.ParseDTimestampTZ(nil, row[i].(string), pgtree.TimeFamilyPrecisionToRoundDuration(6), time.UTC); err == nil {
|
|
newRow[i] = functions.FormatDateTimeWithBC(t.Time.UTC(), "2006-01-02", dt.ID == types.TimestampTZ.ID)
|
|
}
|
|
}
|
|
} else if dt.ID == types.Timestamp.ID || dt.ID == types.TimestampTZ.ID {
|
|
newRow[i] = row[i]
|
|
if row[i] != nil {
|
|
if t, _, err := pgtree.ParseDTimestampTZ(nil, row[i].(string), pgtree.TimeFamilyPrecisionToRoundDuration(6), time.UTC); err == nil {
|
|
newRow[i] = functions.FormatDateTimeWithBC(t.Time.UTC(), "2006-01-02 15:04:05.999999", dt.ID == types.TimestampTZ.ID)
|
|
}
|
|
}
|
|
} else {
|
|
newRow[i] = NormalizeIntsAndFloats(row[i])
|
|
}
|
|
}
|
|
newRows[ri] = newRow
|
|
}
|
|
}
|
|
return newRows
|
|
}
|
|
|
|
// UnmarshalAndMarshalJsonString is used to normalize expected json type value to compare the actual value.
|
|
// JSON type value is in string format, and since Postrges JSON type preserves the input string if valid,
|
|
// it cannot be compared to the returned map as json.Marshal method space padded key value pair.
|
|
// To allow result matching, we unmarshal and marshal the expected string. This causes missing check
|
|
// for the identical format as the input of the json string.
|
|
func UnmarshalAndMarshalJsonString(val string) string {
|
|
var decoded any
|
|
err := json.Unmarshal([]byte(val), &decoded)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ret, err := json.Marshal(decoded)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(ret)
|
|
}
|
|
|
|
// NormalizeValToString normalizes values into types that can be compared.
|
|
// JSON types, any pg types and time and decimal type values are converted into string value.
|
|
// |normalizeNumeric| defines whether to normalize Numeric values into either Numeric type or string type.
|
|
// There are an infinite number of ways to represent the same value in-memory,
|
|
// so we must at least normalize Numeric values.
|
|
func NormalizeValToString(dt *types.DoltgresType, v any) any {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
|
|
switch dt.ID {
|
|
case types.Json.ID:
|
|
jsonBytes, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(jsonBytes)
|
|
case types.JsonB.ID:
|
|
// JSONB normalizes JSON with spaces after ':' and ',' (PostgreSQL JSONB format)
|
|
s, err := gmstypes.JSONDocument{Val: v}.JSONString()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return s
|
|
case types.InternalChar.ID:
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
var b []byte
|
|
if v.(int32) == 0 {
|
|
b = []byte{}
|
|
} else {
|
|
b = []byte{uint8(v.(int32))}
|
|
}
|
|
val, err := dt.FormatValue(string(b))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return val
|
|
case types.Interval.ID, types.Uuid.ID:
|
|
// These values need to be normalized into the appropriate types
|
|
// before being converted to string type using the Doltgres
|
|
// IoOutput method.
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
tVal, err := dt.FormatValue(NormalizeVal(dt, v))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return tVal
|
|
case types.Date.ID:
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return functions.FormatDateTimeWithBC(v.(time.Time), "2006-01-02", false)
|
|
case types.Timestamp.ID, types.TimestampTZ.ID:
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
return functions.FormatDateTimeWithBC(v.(time.Time).UTC(), "2006-01-02 15:04:05.999999", dt.ID == types.TimestampTZ.ID)
|
|
}
|
|
|
|
switch val := v.(type) {
|
|
case bool:
|
|
if val {
|
|
return "t"
|
|
} else {
|
|
return "f"
|
|
}
|
|
case pgtype.Time:
|
|
return timeofday.TimeOfDay(val.Microseconds).String()
|
|
case []any:
|
|
if dt.IsArrayType() {
|
|
return NormalizeArrayType(dt, val)
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// NormalizeArrayType normalizes array types by normalizing its elements first,
|
|
// then to a string using the type IoOutput method.
|
|
func NormalizeArrayType(dt *types.DoltgresType, arr []any) any {
|
|
newVal := make([]any, len(arr))
|
|
for i, el := range arr {
|
|
newVal[i] = NormalizeVal(dt.ArrayBaseType(), el)
|
|
}
|
|
baseType := dt.ArrayBaseType()
|
|
if baseType.ID == types.Bool.ID {
|
|
sqlVal, err := dt.SQL(sql.NewEmptyContext(), nil, newVal)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return sqlVal.ToString()
|
|
} else {
|
|
ret, err := dt.FormatValue(newVal)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ret
|
|
}
|
|
}
|
|
|
|
// NormalizeVal normalizes values to the Doltgres type expects, so it can be used to
|
|
// convert the values using the given Doltgres type. This is used to normalize array
|
|
// types as the type conversion expects certain type values.
|
|
func NormalizeVal(dt *types.DoltgresType, v any) any {
|
|
switch dt.ID {
|
|
case types.Json.ID:
|
|
str, err := json.Marshal(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(str)
|
|
case types.JsonB.ID:
|
|
return gmstypes.JSONDocument{Val: v}
|
|
case types.Oid.ID, types.Regclass.ID, types.Regproc.ID, types.Regtype.ID:
|
|
if uval, ok := v.(uint32); ok {
|
|
if internalID := id.Cache().ToInternal(uval); internalID.IsValid() {
|
|
return internalID
|
|
}
|
|
return id.NewOID(uval).AsId()
|
|
}
|
|
}
|
|
|
|
switch val := v.(type) {
|
|
case pgtype.Numeric:
|
|
if val.NaN {
|
|
return math.NaN()
|
|
} else if val.InfinityModifier != pgtype.Finite {
|
|
return math.Inf(int(val.InfinityModifier))
|
|
} else if !val.Valid {
|
|
return nil
|
|
} else {
|
|
return apd.New(val.Int.Int64(), val.Exp)
|
|
}
|
|
case pgtype.Time:
|
|
// This value type is used for TIME type.
|
|
return timeofday.FromInt(val.Microseconds)
|
|
case pgtype.Interval:
|
|
// This value type is used for INTERVAL type.
|
|
return duration.MakeDuration(val.Microseconds*functions.NanosPerMicro, int64(val.Days), int64(val.Months))
|
|
case [16]byte:
|
|
// This value type is used for UUID type.
|
|
u, err := uuid.FromBytes(val[:])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return u
|
|
case []any:
|
|
baseType := dt
|
|
if baseType.IsArrayType() {
|
|
baseType = baseType.ArrayBaseType()
|
|
}
|
|
newVal := make([]any, len(val))
|
|
for i, el := range val {
|
|
newVal[i] = NormalizeVal(baseType, el)
|
|
}
|
|
return newVal
|
|
}
|
|
return v
|
|
}
|
|
|
|
// NormalizeIntsAndFloats normalizes all int and float types
|
|
// to int64 and float64, respectively.
|
|
func NormalizeIntsAndFloats(v any) any {
|
|
switch val := v.(type) {
|
|
case int:
|
|
return int64(val)
|
|
case int8:
|
|
return int64(val)
|
|
case int16:
|
|
return int64(val)
|
|
case int32:
|
|
return int64(val)
|
|
case uint:
|
|
return int64(val)
|
|
case uint8:
|
|
return int64(val)
|
|
case uint16:
|
|
return int64(val)
|
|
case uint32:
|
|
return int64(val)
|
|
case uint64:
|
|
// PostgreSQL does not support an uint64 type, so we can always convert this to an int64 safely.
|
|
return int64(val)
|
|
case float32:
|
|
return float64(val)
|
|
case []any:
|
|
for i := range val {
|
|
val[i] = NormalizeIntsAndFloats(val[i])
|
|
}
|
|
return val
|
|
default:
|
|
return val
|
|
}
|
|
}
|
|
|
|
// Numeric creates a numeric value from a string.
|
|
func Numeric(str string) pgtype.Numeric {
|
|
numeric := pgtype.Numeric{}
|
|
if err := numeric.Scan(str); err != nil {
|
|
panic(err)
|
|
}
|
|
return numeric
|
|
}
|
|
|
|
// Timestamp is a helper function to convert timestamp strings to pgtype.Timestamp instances. If
|
|
// the string cannot be converted, this function will panic.
|
|
func Timestamp(timestampStr string) (ret pgtype.Timestamp) {
|
|
t, err := time.Parse("2006-01-02 15:04:05", timestampStr)
|
|
if err != nil {
|
|
panic("invalid timestamp format: " + err.Error())
|
|
}
|
|
if err := ret.Scan(t); err != nil {
|
|
panic("failed to set pgtype.Timestamp: " + err.Error())
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Date is a helper function to convert date strings to pgtype.Date instances. If the string
|
|
// cannot be converted, this function will panic.
|
|
func Date(dateStr string) (d pgtype.Date) {
|
|
t, err := time.Parse("2006-01-02", dateStr)
|
|
if err != nil {
|
|
panic("invalid date format: " + err.Error())
|
|
}
|
|
if err := d.Scan(t); err != nil {
|
|
panic("failed to set pgtype.Date: " + err.Error())
|
|
}
|
|
return d
|
|
}
|
|
|
|
// UUID is a helper function to convert UUID strings to pgtype.UUID instances. If the string
|
|
// cannot be converted, this function will panic.
|
|
func UUID(s string) (u pgtype.UUID) {
|
|
if err := u.Scan(s); err != nil {
|
|
panic(err)
|
|
}
|
|
return u
|
|
}
|
|
|
|
// Connect replaces the Current connection with a new one, using the given username and password. If the username is
|
|
// empty, then the default connection is used. If the username and password match the existing connection, then no new
|
|
// connection is made.
|
|
func (conn *Connection) Connect(ctx context.Context, username string, password string) error {
|
|
// Reuse the existing connection if it's the same username and password
|
|
if username == conn.Username && password == conn.Password && conn.Current != nil {
|
|
return nil
|
|
}
|
|
// Username or password has changed, so we'll close the Current connection only if it's not the default
|
|
if conn.Username != "" && conn.Current != nil {
|
|
_ = conn.Current.Close(ctx)
|
|
}
|
|
conn.Username = username
|
|
conn.Password = password
|
|
if username == "" {
|
|
conn.Current = conn.Default
|
|
return nil
|
|
} else {
|
|
var err error
|
|
config := conn.Default.Config()
|
|
conn.Current, err = pgx.Connect(ctx, fmt.Sprintf("postgres://%s:%s@127.0.0.1:%d/%s", username, password, config.Port, config.Database))
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Exec calls Exec on the current connection.
|
|
func (conn *Connection) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
|
|
if conn.Current == nil {
|
|
return pgconn.CommandTag{}, errors.New("EXEC: current connection is nil")
|
|
}
|
|
return conn.Current.Exec(ctx, sql, args...)
|
|
}
|
|
|
|
// Query calls Query on the current connection.
|
|
func (conn *Connection) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) {
|
|
if conn.Current == nil {
|
|
return nil, errors.New("QUERY: current connection is nil")
|
|
}
|
|
return conn.Current.Query(ctx, sql, args...)
|
|
}
|
|
|
|
// Close closes the connections.
|
|
func (conn *Connection) Close(ctx context.Context) {
|
|
if conn.Default != nil {
|
|
_ = conn.Default.Close(ctx)
|
|
}
|
|
if conn.Current != nil && conn.Current != conn.Default {
|
|
_ = conn.Current.Close(ctx)
|
|
}
|
|
conn.Default = nil
|
|
conn.Current = nil
|
|
}
|