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

830 lines
26 KiB
Go

// 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 server
import (
"context"
"encoding/base64"
goerrors "errors"
"fmt"
"io"
"os"
"regexp"
"runtime/debug"
"runtime/trace"
"strings"
"sync"
"time"
"github.com/cockroachdb/errors"
sqle "github.com/dolthub/go-mysql-server"
"github.com/dolthub/go-mysql-server/server"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/analyzer"
"github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/types"
"github.com/dolthub/vitess/go/mysql"
"github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/jackc/pgx/v5/pgproto3"
"github.com/jackc/pgx/v5/pgtype"
"github.com/sirupsen/logrus"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/server/ast"
"github.com/dolthub/doltgresql/server/auth"
pgexprs "github.com/dolthub/doltgresql/server/expression"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
var (
printErrorStackTraces = false
ForceTextWireFormat = false
)
const (
PrintErrorStackTracesEnvKey = "DOLTGRES_PRINT_ERROR_STACK_TRACES"
ForceTextWireFormatEnvKey = "DOLTGRES_FORCE_TEXT_WIRE_FORMAT"
)
func init() {
if _, ok := os.LookupEnv(PrintErrorStackTracesEnvKey); ok {
printErrorStackTraces = true
}
if _, ok := os.LookupEnv(ForceTextWireFormatEnvKey); ok {
ForceTextWireFormat = true
}
}
// BindVariables represents arrays of types, format codes and parameters
// used to convert given parameters to binding variables map.
type BindVariables struct {
varTypes []uint32
formatCodes []int16
parameters [][]byte
}
// Result represents a query result.
type Result struct {
Fields []pgproto3.FieldDescription `json:"fields"`
Rows []Row `json:"rows"`
RowsAffected uint64 `json:"rows_affected"`
}
// Row represents a single row value in bytes format.
// |val| represents array of a single row elements,
// which each element value is in byte array format.
type Row struct {
val [][]byte
}
const rowsBatch = 128
// DoltgresHandler is a handler uses SQLe engine directly
// running Doltgres specific queries.
type DoltgresHandler struct {
e *sqle.Engine
sm *server.SessionManager
readTimeout time.Duration
encodeLoggedQuery bool
pgTypeMap *pgtype.Map
sel server.ServerEventListener
}
var _ Handler = &DoltgresHandler{}
// ComBind implements the Handler interface.
func (h *DoltgresHandler) ComBind(ctx context.Context, c *mysql.Conn, query string, parsedQuery mysql.ParsedQuery, bindVars BindVariables, formatCodes []int16) (mysql.BoundQuery, []pgproto3.FieldDescription, error) {
sqlCtx, err := h.sm.NewContextWithQuery(ctx, c, query)
if err != nil {
return nil, nil, err
}
stmt, ok := parsedQuery.(sqlparser.Statement)
if !ok {
return nil, nil, errors.Errorf("parsedQuery must be a sqlparser.Statement, but got %T", parsedQuery)
}
bvs, err := h.convertBindParameters(sqlCtx, bindVars.varTypes, bindVars.formatCodes, bindVars.parameters)
if err != nil {
if printErrorStackTraces {
fmt.Printf("unable to convert bind params: %+v\n", err)
}
return nil, nil, err
}
queryPlan, err := h.e.BoundQueryPlan(sqlCtx, query, stmt, bvs)
if err != nil {
if printErrorStackTraces {
fmt.Printf("unable to bind query plan: %+v\n", err)
}
return nil, nil, err
}
fields, err := schemaToFieldDescriptions(sqlCtx, queryPlan.Schema(sqlCtx), formatCodes)
return queryPlan, fields, err
}
// ComExecuteBound implements the Handler interface.
func (h *DoltgresHandler) ComExecuteBound(ctx context.Context, conn *mysql.Conn, query string, boundQuery mysql.BoundQuery, formatCodes []int16, callback func(*sql.Context, *Result) error) error {
analyzedPlan, ok := boundQuery.(sql.Node)
if !ok {
return errors.Errorf("boundQuery must be a sql.Node, but got %T", boundQuery)
}
// TODO: This technically isn't query start and underestimates query execution time
start := time.Now()
if h.sel != nil {
h.sel.QueryStarted()
}
err := h.doQuery(ctx, conn, query, nil, analyzedPlan, h.executeBoundPlan, callback, formatCodes)
if err != nil {
err = sql.CastSQLError(err)
}
if h.sel != nil {
h.sel.QueryCompleted(err == nil, time.Since(start))
}
return err
}
// ComPrepareParsed implements the Handler interface.
func (h *DoltgresHandler) ComPrepareParsed(ctx context.Context, c *mysql.Conn, query string, parsed sqlparser.Statement) (mysql.ParsedQuery, []pgproto3.FieldDescription, error) {
sqlCtx, ok := ctx.(*sql.Context)
if !ok {
var err error
sqlCtx, err = h.sm.NewContextWithQuery(ctx, c, query)
if err != nil {
return nil, nil, err
}
}
node, err := h.e.PrepareParsedQuery(sqlCtx, query, query, parsed)
if err != nil {
if printErrorStackTraces {
fmt.Printf("unable to prepare query: %+v\n", err)
}
logrus.WithField("query", query).Errorf("unable to prepare query: %s", err.Error())
return nil, nil, sql.CastSQLError(err)
}
// Always attempt analysis to get correct column names for Describe(statement) responses.
// When bind variables are present the analyzer may fail or produce an inaccurate schema;
// in that case we fall back to the unanalyzed node rather than propagating the error —
// the real analysis with bound values happens later during Bind/Execute.
//
// Importantly, we return the pre-analysis node regardless of whether analysis succeeded.
// The analyzed plan is used only for schema/column-name determination; the unanalyzed node
// must be returned so that the caller can still find BindVar expressions via
// extractBindVarTypes (the analyzer replaces them with typed expressions).
analyzed := node
if attemptedAnalysis, analyzeErr := h.e.Analyzer.Analyze(sqlCtx, node, nil, nil); analyzeErr == nil {
analyzed = attemptedAnalysis
}
var fields []pgproto3.FieldDescription
// The query is not a SELECT statement if it corresponds to an OK result.
if nodeReturnsOkResultSchema(sqlCtx, analyzed) {
fields = []pgproto3.FieldDescription{
{
Name: []byte("Rows"),
DataTypeOID: id.Cache().ToOID(pgtypes.Int32.ID.AsId()),
DataTypeSize: int16(pgtypes.Int32.MaxTextResponseByteLength(nil)),
},
}
} else {
fields, err = schemaToFieldDescriptions(sqlCtx, analyzed.Schema(sqlCtx), nil)
if err != nil {
return nil, nil, err
}
}
// Return the pre-analysis node so callers can still find BindVar expressions.
// The analyzed plan was only needed to determine column names for the fields above.
return node, fields, nil
}
// ComQuery implements the Handler interface.
func (h *DoltgresHandler) ComQuery(ctx context.Context, c *mysql.Conn, query string, parsed sqlparser.Statement, callback func(*sql.Context, *Result) error) error {
// TODO: This technically isn't query start and underestimates query execution time
start := time.Now()
if h.sel != nil {
h.sel.QueryStarted()
}
err := h.doQuery(ctx, c, query, parsed, nil, h.executeQuery, callback, nil)
if err != nil {
err = sql.CastSQLError(err)
}
if h.sel != nil {
h.sel.QueryCompleted(err == nil, time.Since(start))
}
return err
}
// ComResetConnection implements the Handler interface.
func (h *DoltgresHandler) ComResetConnection(c *mysql.Conn) error {
logrus.WithField("connectionId", c.ConnectionID).Debug("COM_RESET_CONNECTION command received")
// Grab the currently selected database name
db := h.sm.GetCurrentDB(c)
// Dispose of the connection's current session
h.maybeReleaseAllLocks(c)
h.e.CloseSession(c.ConnectionID)
ctx := context.Background()
// Create a new session and set the current database
err := h.sm.NewSession(ctx, c)
if err != nil {
return err
}
return h.sm.SetDB(ctx, c, db)
}
// ConnectionClosed implements the Handler interface.
func (h *DoltgresHandler) ConnectionClosed(c *mysql.Conn) {
defer func() {
if h.sel != nil {
h.sel.ClientDisconnected()
}
}()
defer h.sm.RemoveConn(c)
defer h.e.CloseSession(c.ConnectionID)
h.maybeReleaseAllLocks(c)
logrus.WithField(sql.ConnectionIdLogField, c.ConnectionID).Infof("ConnectionClosed")
}
// NewConnection implements the Handler interface.
func (h *DoltgresHandler) NewConnection(c *mysql.Conn) {
if h.sel != nil {
h.sel.ClientConnected()
}
h.sm.AddConn(c)
sql.StatusVariables.IncrementGlobal("Connections", 1)
c.DisableClientMultiStatements = true // TODO: h.disableMultiStmts
logrus.WithField(sql.ConnectionIdLogField, c.ConnectionID).WithField("DisableClientMultiStatements", c.DisableClientMultiStatements).Infof("NewConnection")
}
// NewContext implements the Handler interface.
func (h *DoltgresHandler) NewContext(ctx context.Context, c *mysql.Conn, query string) (*sql.Context, error) {
return h.sm.NewContextWithQuery(ctx, c, query)
}
// InitSessionParameterDefault sets a default value to specified parameter for a session.
func (h *DoltgresHandler) InitSessionParameterDefault(ctx context.Context, c *mysql.Conn, name, value string) error {
return h.sm.InitSessionDefaultVariable(ctx, c, name, value)
}
// convertBindParameters handles the conversion from bind parameters to variable values.
func (h *DoltgresHandler) convertBindParameters(ctx *sql.Context, types []uint32, formatCodes []int16, values [][]byte) (map[string]sqlparser.Expr, error) {
bindings := make(map[string]sqlparser.Expr, len(values))
// It's valid to send just one format code that should be used by all values, so we extend the slice in that case
formatCodes, err := extendFormatCodes(len(values), formatCodes)
if err != nil {
return nil, err
}
typeColl, err := core.GetTypesCollectionFromContext(ctx, "")
if err != nil {
return nil, err
}
for i := range values {
formatCode := formatCodes[i]
dgType, err := typeColl.GetType(ctx, id.Type(id.Cache().ToInternal(types[i])))
if err != nil {
return nil, err
}
if values[i] != nil {
if formatCode == 0 {
v, err := dgType.IoInput(ctx, string(values[i]))
if err != nil {
return nil, err
}
bindings[fmt.Sprintf("v%d", i+1)] = sqlparser.InjectedExpr{Expression: pgexprs.NewUnsafeLiteral(v, dgType)}
} else {
v, err := dgType.CallReceive(ctx, values[i])
if err != nil {
return nil, err
}
bindings[fmt.Sprintf("v%d", i+1)] = sqlparser.InjectedExpr{Expression: pgexprs.NewUnsafeLiteral(v, dgType)}
}
} else {
bindings[fmt.Sprintf("v%d", i+1)] = sqlparser.InjectedExpr{Expression: pgexprs.NewUnsafeLiteral(nil, dgType)}
}
}
return bindings, nil
}
var queryLoggingRegex = regexp.MustCompile(`[\r\n\t ]+`)
func (h *DoltgresHandler) doQuery(ctx context.Context, c *mysql.Conn, query string, parsed sqlparser.Statement, analyzedPlan sql.Node, queryExec QueryExecutor, callback func(*sql.Context, *Result) error, formatCodes []int16) error {
sqlCtx, err := h.sm.NewContextWithQuery(ctx, c, query)
if err != nil {
return err
}
sqlCtx.SetPrivilegeSet(auth.NewPrivilegeSetLayer(sqlCtx), 1)
start := time.Now()
var queryStrToLog string
if h.encodeLoggedQuery {
queryStrToLog = base64.StdEncoding.EncodeToString([]byte(query))
} else if logrus.IsLevelEnabled(logrus.DebugLevel) {
// this is expensive, so skip this unless we're logging at DEBUG level
queryStrToLog = string(queryLoggingRegex.ReplaceAll([]byte(query), []byte(" ")))
}
if queryStrToLog != "" {
sqlCtx.SetLogger(sqlCtx.GetLogger().WithField("query", queryStrToLog))
}
sqlCtx.GetLogger().Debugf("Starting query")
sqlCtx.GetLogger().Tracef("beginning execution")
// TODO: it would be nice to put this logic in the engine, not the handler, but we don't want the process to be
// marked done until we're done spooling rows over the wire
lgr := sqlCtx.GetLogger()
sqlCtx, err = sqlCtx.ProcessList.BeginQuery(sqlCtx, query)
if err != nil {
lgr.WithError(err).Warn("error running query; could not open process list context")
return err
}
defer sqlCtx.ProcessList.EndQuery(sqlCtx)
schema, rowIter, qFlags, err := queryExec(sqlCtx, query, parsed, analyzedPlan)
if err != nil {
if printErrorStackTraces {
fmt.Printf("error running query: %+v\n", err)
}
sqlCtx.GetLogger().WithError(err).Warn("error running query")
return err
}
// create result before goroutines to avoid |ctx| racing
var r *Result
var processedAtLeastOneBatch bool
// zero/single return schema use spooling shortcut
if types.IsOkResultSchema(schema) {
r, err = resultForOkIter(sqlCtx, rowIter)
if err != nil {
return err
}
} else if schema == nil {
r, err = resultForEmptyIter(sqlCtx, rowIter)
if err != nil {
return err
}
} else if analyzer.FlagIsSet(qFlags, sql.QFlagMax1Row) {
resultFields, err := schemaToFieldDescriptions(sqlCtx, schema, formatCodes)
if err != nil {
return err
}
r, err = resultForMax1RowIter(sqlCtx, schema, rowIter, resultFields, formatCodes)
if err != nil {
return err
}
} else {
resultFields, err := schemaToFieldDescriptions(sqlCtx, schema, formatCodes)
if err != nil {
return err
}
r, processedAtLeastOneBatch, err = h.resultForDefaultIter(sqlCtx, schema, rowIter, callback, resultFields, formatCodes)
if err != nil {
return err
}
}
sqlCtx.GetLogger().Debugf("Query finished in %d ms", time.Since(start).Milliseconds())
// processedAtLeastOneBatch means we already called callback() at least
// once, so no need to call it if RowsAffected == 0.
if r != nil && (r.RowsAffected == 0 && processedAtLeastOneBatch) {
return nil
}
return callback(sqlCtx, r)
}
// QueryExecutor is a function that executes a query and returns the result as a schema and iterator. Either of
// |parsed| or |analyzed| can be nil depending on the use case
type QueryExecutor func(ctx *sql.Context, query string, parsed sqlparser.Statement, analyzed sql.Node) (sql.Schema, sql.RowIter, *sql.QueryFlags, error)
// executeQuery is a QueryExecutor that calls QueryWithBindings on the given engine using the given query and parsed
// statement, which may be nil.
func (h *DoltgresHandler) executeQuery(ctx *sql.Context, query string, parsed sqlparser.Statement, _ sql.Node) (sql.Schema, sql.RowIter, *sql.QueryFlags, error) {
return h.e.QueryWithBindings(ctx, query, parsed, nil, nil)
}
// executeBoundPlan is a QueryExecutor that calls QueryWithBindings on the given engine using the given query and parsed
// statement, which may be nil.
func (h *DoltgresHandler) executeBoundPlan(ctx *sql.Context, query string, _ sqlparser.Statement, plan sql.Node) (sql.Schema, sql.RowIter, *sql.QueryFlags, error) {
return h.e.PrepQueryPlanForExecution(ctx, query, plan, nil)
}
// maybeReleaseAllLocks makes a best effort attempt to release all locks on the given connection. If the attempt fails,
// an error is logged but not returned.
func (h *DoltgresHandler) maybeReleaseAllLocks(c *mysql.Conn) {
if ctx, err := h.sm.NewContextWithQuery(context.Background(), c, ""); err != nil {
logrus.Errorf("unable to release all locks on session close: %s", err)
logrus.Errorf("unable to unlock tables on session close: %s", err)
} else {
_, err = h.e.LS.ReleaseAll(ctx)
if err != nil {
logrus.Errorf("unable to release all locks on session close: %s", err)
}
if err = h.e.Analyzer.Catalog.UnlockTables(ctx, c.ConnectionID); err != nil {
logrus.Errorf("unable to unlock tables on session close: %s", err)
}
}
}
// nodeReturnsOkResultSchema returns whether the node returns OK result or the schema is OK result schema.
// These nodes will eventually return an OK result, but their intermediate forms here return a different schema
// than they will at execution time.
func nodeReturnsOkResultSchema(ctx *sql.Context, node sql.Node) bool {
switch n := node.(type) {
case *plan.InsertInto:
return len(n.Returning) == 0
case *plan.Update:
return len(n.Returning) == 0
case *plan.DeleteFrom, *plan.UpdateJoin:
return true
}
return types.IsOkResultSchema(node.Schema(ctx))
}
// extendFormatCodes ensures that the given format codes match the expected field length by extending the short-form
// variant (or returning the full-form as-is).
func extendFormatCodes(fieldLength int, formatCodes []int16) ([]int16, error) {
if !ForceTextWireFormat && len(formatCodes) > 0 {
// It's valid to send just one format code that should be used by all values, so we extend the slice in that case
if len(formatCodes) < fieldLength {
if len(formatCodes) > 1 {
return nil, errors.Errorf(`format codes have length "%d" but fields have length "%d"`, len(formatCodes), fieldLength)
}
newFormatCodes := make([]int16, fieldLength)
for i := range newFormatCodes {
newFormatCodes[i] = formatCodes[0]
}
return newFormatCodes, nil
} else {
return formatCodes, nil
}
} else {
// This defaults to zero, which means all format codes represent the "text" option
return make([]int16, fieldLength), nil
}
}
func schemaToFieldDescriptions(ctx *sql.Context, s sql.Schema, formatCodes []int16) ([]pgproto3.FieldDescription, error) {
var err error
formatCodes, err = extendFormatCodes(len(s), formatCodes)
if err != nil {
return nil, err
}
fields := make([]pgproto3.FieldDescription, len(s))
for i, c := range s {
var oid uint32
var typmod = int32(-1)
var err error
colName := c.Name
// Translate the internal sentinel aliases set by nodeSelectExprs for unaliased string
// literals back to Postgres's ?column? placeholder.
if strings.HasPrefix(colName, ast.UnknownColSentinelPrefix) {
colName = "?column?"
}
dataTypeSize := int16(c.Type.MaxTextResponseByteLength(ctx))
tableAttributeNumber := uint16(i + 1) // TODO: this should be based on the actual table field index, not the return schema
if doltgresType, ok := c.Type.(*pgtypes.DoltgresType); ok {
if doltgresType.ID == pgtypes.Unknown.ID {
// The `unknown` type is always converted to `text` on output since they're binary coercible.
// No function or column returns `unknown`, so this is always a raw string literal from the query.
// Column naming (alias vs ?column?) is handled in nodeSelectExprs; preserve whatever name
// was set there.
doltgresType = pgtypes.Text
dataTypeSize = int16(doltgresType.MaxTextResponseByteLength(ctx))
tableAttributeNumber = 0
}
if doltgresType.TypType == pgtypes.TypeType_Domain {
oid = id.Cache().ToOID(doltgresType.BaseTypeType.ID.AsId())
} else {
oid = id.Cache().ToOID(doltgresType.ID.AsId())
}
typmod = doltgresType.GetAttTypMod() // pg_attribute.atttypmod
} else {
oid, err = VitessTypeToObjectID(c.Type)
if err != nil {
panic(err)
}
}
fields[i] = pgproto3.FieldDescription{
Name: []byte(colName),
TableOID: uint32(0),
TableAttributeNumber: tableAttributeNumber,
DataTypeOID: oid,
DataTypeSize: dataTypeSize,
TypeModifier: typmod,
Format: formatCodes[i],
}
}
return fields, nil
}
// resultForOkIter reads a maximum of one result row from a result iterator.
func resultForOkIter(ctx *sql.Context, iter sql.RowIter) (*Result, error) {
defer trace.StartRegion(ctx, "DoltgresHandler.resultForOkIter").End()
row, err := iter.Next(ctx)
if err != nil {
if printErrorStackTraces {
fmt.Printf("row: %+v\n", err)
}
return nil, err
}
_, err = iter.Next(ctx)
if err != io.EOF {
return nil, errors.Errorf("result schema iterator returned more than one row")
}
if err := iter.Close(ctx); err != nil {
return nil, err
}
return &Result{
RowsAffected: row[0].(types.OkResult).RowsAffected,
}, nil
}
// resultForEmptyIter ensures that an expected empty iterator returns no rows.
func resultForEmptyIter(ctx *sql.Context, iter sql.RowIter) (*Result, error) {
defer trace.StartRegion(ctx, "DoltgresHandler.resultForEmptyIter").End()
if _, err := iter.Next(ctx); err != io.EOF {
return nil, errors.Errorf("result schema iterator returned more than zero rows")
}
if err := iter.Close(ctx); err != nil {
return nil, err
}
return &Result{Fields: nil}, nil
}
// resultForMax1RowIter ensures that an empty iterator returns at most one row
func resultForMax1RowIter(ctx *sql.Context, schema sql.Schema, iter sql.RowIter, resultFields []pgproto3.FieldDescription, formatCodes []int16) (*Result, error) {
defer trace.StartRegion(ctx, "DoltgresHandler.resultForMax1RowIter").End()
row, err := iter.Next(ctx)
if err == io.EOF {
return &Result{Fields: resultFields}, nil
} else if err != nil {
return nil, err
}
if _, err = iter.Next(ctx); err != io.EOF {
return nil, errors.Errorf("result max1Row iterator returned more than one row")
}
if err := iter.Close(ctx); err != nil {
return nil, err
}
outputRow, err := rowToBytes(ctx, schema, row, formatCodes)
if err != nil {
return nil, err
}
ctx.GetLogger().Tracef("spooling result row %s", outputRow)
return &Result{Fields: resultFields, Rows: []Row{{outputRow}}, RowsAffected: 1}, nil
}
// resultForDefaultIter reads batches of rows from the iterator
// and writes results into the callback function.
func (h *DoltgresHandler) resultForDefaultIter(ctx *sql.Context, schema sql.Schema, iter sql.RowIter, callback func(*sql.Context, *Result) error, resultFields []pgproto3.FieldDescription, formatCodes []int16) (*Result, bool, error) {
defer trace.StartRegion(ctx, "DoltgresHandler.resultForDefaultIter").End()
// TODO: use errguard.Go instead?
pan2err := func(err *error) {
if HandlePanics {
if recoveredPanic := recover(); recoveredPanic != nil {
if err == nil {
*err = errors.Errorf("DoltgresHandler caught panic with nil error: %v: %s", recoveredPanic, debug.Stack())
} else {
// debug.Stack() here prints the stack trace of the original panic, not the lexical stack of this defer function
*err = goerrors.Join(*err, errors.Errorf("DoltgresHandler caught panic: %v: %s", recoveredPanic, debug.Stack()))
}
}
}
}
wg := sync.WaitGroup{}
wg.Add(3)
eg, ctx := ctx.NewErrgroup()
// Read rows off the row iterator and send them to the row channel.
var rowChan = make(chan sql.Row, 512)
eg.Go(func() (err error) {
defer pan2err(&err)
defer wg.Done()
defer close(rowChan)
for {
select {
case <-ctx.Done():
return context.Cause(ctx)
default:
row, iErr := iter.Next(ctx)
if iErr == io.EOF {
return nil
}
if iErr != nil {
return iErr
}
select {
case rowChan <- row:
case <-ctx.Done():
return nil
}
}
}
})
// Default waitTime is one minute if there is no timeout configured, in which case
// it will loop to iterate again unless the socket died by the OS timeout or other problems.
// If there is a timeout, it will be enforced to ensure that Vitess has a chance to
// call DoltgresHandler.CloseConnection()
waitTime := 1 * time.Minute
if h.readTimeout > 0 {
waitTime = h.readTimeout
}
timer := time.NewTimer(waitTime)
defer timer.Stop()
// Drain rows from rowChan, convert to wire format, and send to resChan
var resChan = make(chan *Result, 4)
var res *Result
eg.Go(func() (err error) {
defer pan2err(&err)
defer wg.Done()
defer close(resChan)
for {
if res == nil {
res = &Result{
Fields: resultFields,
Rows: make([]Row, 0, rowsBatch),
}
}
select {
case <-ctx.Done():
return context.Cause(ctx)
case <-timer.C:
if h.readTimeout != 0 {
// Cancel and return so Vitess can call the CloseConnection callback
ctx.GetLogger().Tracef("connection timeout")
return errors.Errorf("row read wait bigger than connection timeout")
}
case row, ok := <-rowChan:
if !ok {
return nil
}
if types.IsOkResult(row) {
if res.RowsAffected > 0 {
panic("Got OkResult mixed with RowResult")
}
result := row[0].(types.OkResult)
res.Fields = nil
res.Rows = nil
res.RowsAffected = result.RowsAffected
continue
}
outputRow, rErr := rowToBytes(ctx, schema, row, formatCodes)
if rErr != nil {
return rErr
}
ctx.GetLogger().Tracef("spooling result row %s", outputRow)
res.Rows = append(res.Rows, Row{outputRow})
res.RowsAffected++
if res.RowsAffected == rowsBatch {
select {
case <-ctx.Done():
return context.Cause(ctx)
case resChan <- res:
res = nil
}
}
}
timer.Reset(waitTime)
}
})
// Drain Result from resChan and call callback
var processedAtLeastOneBatch bool
eg.Go(func() (err error) {
defer pan2err(&err)
defer wg.Done()
for {
select {
case <-ctx.Done():
return context.Cause(ctx)
case r, ok := <-resChan:
if !ok {
return nil
}
processedAtLeastOneBatch = true
cErr := callback(ctx, r)
if cErr != nil {
return cErr
}
}
}
})
// Close() kills this PID in the process list,
// wait until all rows have be sent over the wire
eg.Go(func() (err error) {
defer pan2err(&err)
wg.Wait()
return iter.Close(ctx)
})
err := eg.Wait()
if err != nil {
if printErrorStackTraces {
fmt.Printf("error running query: %+v\n", err)
}
ctx.GetLogger().WithError(err).Warn("error running query")
return nil, false, err
}
return res, processedAtLeastOneBatch, nil
}
func rowToBytes(ctx *sql.Context, s sql.Schema, row sql.Row, formatCodes []int16) ([][]byte, error) {
if len(row) == 0 {
return nil, nil
}
if len(s) == 0 {
// should not happen
return nil, errors.Errorf("received empty schema")
}
formatCodes, err := extendFormatCodes(len(row), formatCodes)
if err != nil {
return nil, err
}
o := make([][]byte, len(row))
for i, v := range row {
if v == nil {
o[i] = nil
} else if formatCodes[i] == 1 {
switch d := s[i].Type.(type) {
case *pgtypes.DoltgresType:
o[i], err = d.CallSend(ctx, v)
if err != nil {
return nil, err
}
default:
cast := pgexprs.NewGMSCast(expression.NewLiteral(v, d))
v, err = cast.Eval(ctx, nil)
if err != nil {
return nil, err
}
o[i], err = cast.DoltgresType(ctx).CallSend(ctx, v)
if err != nil {
return nil, err
}
}
} else {
val, err := s[i].Type.SQL(ctx, []byte{}, v) // We use []byte{} as there's a distinction between nil and empty
if err != nil {
return nil, err
}
o[i] = val.ToBytes()
}
}
return o, nil
}