830 lines
26 KiB
Go
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
|
|
}
|