*: show create table output utf8_bin collate. We only support case sensitive utf8_bin collation right now, but show create table ignored the collation. So the dumped DDL statement doesn't contains collation option. When the DDL statement is executed on MySQL, the collation will be changed to utf8_general_ci. If the string column is defined as unique key, there would be duplicated key error. This PR enforce utf8 charset and utf8_bin collation when parse a column definition, and enfore table option `DEFAULT CHARSET=utf8 COLLATE=utf8_bin` in SHOW CREATE TABLE statement.
629 lines
16 KiB
Go
629 lines
16 KiB
Go
// Copyright 2016 PingCAP, 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,
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package executor
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/juju/errors"
|
|
"github.com/pingcap/tidb/ast"
|
|
"github.com/pingcap/tidb/context"
|
|
"github.com/pingcap/tidb/expression"
|
|
"github.com/pingcap/tidb/infoschema"
|
|
"github.com/pingcap/tidb/model"
|
|
"github.com/pingcap/tidb/mysql"
|
|
"github.com/pingcap/tidb/privilege"
|
|
"github.com/pingcap/tidb/sessionctx/variable"
|
|
"github.com/pingcap/tidb/sessionctx/varsutil"
|
|
"github.com/pingcap/tidb/table"
|
|
"github.com/pingcap/tidb/terror"
|
|
"github.com/pingcap/tidb/util/charset"
|
|
"github.com/pingcap/tidb/util/types"
|
|
)
|
|
|
|
// ShowExec represents a show executor.
|
|
type ShowExec struct {
|
|
Tp ast.ShowStmtType // Databases/Tables/Columns/....
|
|
DBName model.CIStr
|
|
Table *ast.TableName // Used for showing columns.
|
|
Column *ast.ColumnName // Used for `desc table column`.
|
|
Flag int // Some flag parsed from sql, such as FULL.
|
|
Full bool
|
|
User string // Used for show grants.
|
|
|
|
// Used by show variables
|
|
GlobalScope bool
|
|
|
|
schema *expression.Schema
|
|
ctx context.Context
|
|
is infoschema.InfoSchema
|
|
|
|
fetched bool
|
|
rows []*Row
|
|
cursor int
|
|
}
|
|
|
|
// Schema implements the Executor Schema interface.
|
|
func (e *ShowExec) Schema() *expression.Schema {
|
|
return e.schema
|
|
}
|
|
|
|
// Next implements Execution Next interface.
|
|
func (e *ShowExec) Next() (*Row, error) {
|
|
if e.rows == nil {
|
|
err := e.fetchAll()
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
}
|
|
if e.cursor >= len(e.rows) {
|
|
return nil, nil
|
|
}
|
|
row := e.rows[e.cursor]
|
|
e.cursor++
|
|
return row, nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchAll() error {
|
|
switch e.Tp {
|
|
case ast.ShowCharset:
|
|
return e.fetchShowCharset()
|
|
case ast.ShowCollation:
|
|
return e.fetchShowCollation()
|
|
case ast.ShowColumns:
|
|
return e.fetchShowColumns()
|
|
case ast.ShowCreateTable:
|
|
return e.fetchShowCreateTable()
|
|
case ast.ShowCreateDatabase:
|
|
return e.fetchShowCreateDatabase()
|
|
case ast.ShowDatabases:
|
|
return e.fetchShowDatabases()
|
|
case ast.ShowEngines:
|
|
return e.fetchShowEngines()
|
|
case ast.ShowGrants:
|
|
return e.fetchShowGrants()
|
|
case ast.ShowIndex:
|
|
return e.fetchShowIndex()
|
|
case ast.ShowProcedureStatus:
|
|
return e.fetchShowProcedureStatus()
|
|
case ast.ShowStatus:
|
|
return e.fetchShowStatus()
|
|
case ast.ShowTables:
|
|
return e.fetchShowTables()
|
|
case ast.ShowTableStatus:
|
|
return e.fetchShowTableStatus()
|
|
case ast.ShowTriggers:
|
|
return e.fetchShowTriggers()
|
|
case ast.ShowVariables:
|
|
return e.fetchShowVariables()
|
|
case ast.ShowWarnings:
|
|
return e.fetchShowWarnings()
|
|
case ast.ShowProcessList:
|
|
return e.fetchShowProcessList()
|
|
case ast.ShowEvents:
|
|
// empty result
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowEngines() error {
|
|
row := &Row{
|
|
Data: types.MakeDatums(
|
|
"InnoDB",
|
|
"DEFAULT",
|
|
"Supports transactions, row-level locking, and foreign keys",
|
|
"YES",
|
|
"YES",
|
|
"YES",
|
|
),
|
|
}
|
|
e.rows = append(e.rows, row)
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowDatabases() error {
|
|
dbs := e.is.AllSchemaNames()
|
|
checker := privilege.GetPrivilegeManager(e.ctx)
|
|
// TODO: let information_schema be the first database
|
|
sort.Strings(dbs)
|
|
for _, d := range dbs {
|
|
if checker != nil && !checker.DBIsVisible(d) {
|
|
continue
|
|
}
|
|
e.rows = append(e.rows, &Row{Data: types.MakeDatums(d)})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowProcessList() error {
|
|
sm := e.ctx.GetSessionManager()
|
|
if sm == nil {
|
|
return nil
|
|
}
|
|
|
|
pl := sm.ShowProcessList()
|
|
for _, pi := range pl {
|
|
var t uint64
|
|
if len(pi.Info) != 0 {
|
|
t = uint64(time.Since(pi.Time) / time.Second)
|
|
}
|
|
row := &Row{
|
|
Data: []types.Datum{
|
|
types.NewUintDatum(pi.ID),
|
|
types.NewStringDatum(pi.User),
|
|
types.NewStringDatum(pi.Host),
|
|
types.NewStringDatum(pi.DB),
|
|
types.NewStringDatum(pi.Command),
|
|
types.NewUintDatum(t),
|
|
types.NewStringDatum(fmt.Sprintf("%d", pi.State)),
|
|
types.NewStringDatum(pi.Info),
|
|
},
|
|
}
|
|
e.rows = append(e.rows, row)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowTables() error {
|
|
if !e.is.SchemaExists(e.DBName) {
|
|
return errors.Errorf("Can not find DB: %s", e.DBName)
|
|
}
|
|
checker := privilege.GetPrivilegeManager(e.ctx)
|
|
// sort for tables
|
|
var tableNames []string
|
|
for _, v := range e.is.SchemaTables(e.DBName) {
|
|
// Test with mysql.AllPrivMask means any privilege would be OK.
|
|
// TODO: Should consider column privileges, which also make a table visible.
|
|
if checker != nil && !checker.RequestVerification(e.DBName.O, v.Meta().Name.O, "", mysql.AllPrivMask) {
|
|
continue
|
|
}
|
|
tableNames = append(tableNames, v.Meta().Name.O)
|
|
}
|
|
sort.Strings(tableNames)
|
|
for _, v := range tableNames {
|
|
data := types.MakeDatums(v)
|
|
if e.Full {
|
|
// TODO: support "VIEW" later if we have supported view feature.
|
|
// now, just use "BASE TABLE".
|
|
data = append(data, types.NewDatum("BASE TABLE"))
|
|
}
|
|
e.rows = append(e.rows, &Row{Data: data})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowTableStatus() error {
|
|
if !e.is.SchemaExists(e.DBName) {
|
|
return errors.Errorf("Can not find DB: %s", e.DBName)
|
|
}
|
|
|
|
// sort for tables
|
|
tables := e.is.SchemaTables(e.DBName)
|
|
sort.Sort(table.Slice(tables))
|
|
|
|
for _, t := range tables {
|
|
now := types.CurrentTime(mysql.TypeDatetime)
|
|
data := types.MakeDatums(t.Meta().Name.O, "InnoDB", "10", "Compact", 100, 100, 100, 100, 100, 100, 100,
|
|
now, now, now, "utf8_general_ci", "", "", t.Meta().Comment)
|
|
e.rows = append(e.rows, &Row{Data: data})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowColumns() error {
|
|
tb, err := e.getTable()
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
cols := tb.Cols()
|
|
for _, col := range cols {
|
|
if e.Column != nil && e.Column.Name.L != col.Name.L {
|
|
continue
|
|
}
|
|
|
|
desc := table.NewColDesc(col)
|
|
|
|
// The FULL keyword causes the output to include the column collation and comments,
|
|
// as well as the privileges you have for each column.
|
|
row := &Row{}
|
|
if e.Full {
|
|
row.Data = types.MakeDatums(
|
|
desc.Field,
|
|
desc.Type,
|
|
desc.Collation,
|
|
desc.Null,
|
|
desc.Key,
|
|
desc.DefaultValue,
|
|
desc.Extra,
|
|
desc.Privileges,
|
|
desc.Comment,
|
|
)
|
|
} else {
|
|
row.Data = types.MakeDatums(
|
|
desc.Field,
|
|
desc.Type,
|
|
desc.Null,
|
|
desc.Key,
|
|
desc.DefaultValue,
|
|
desc.Extra,
|
|
)
|
|
}
|
|
e.rows = append(e.rows, row)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowIndex() error {
|
|
tb, err := e.getTable()
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
if tb.Meta().PKIsHandle {
|
|
var pkCol *table.Column
|
|
for _, col := range tb.Cols() {
|
|
if mysql.HasPriKeyFlag(col.Flag) {
|
|
pkCol = col
|
|
break
|
|
}
|
|
}
|
|
data := types.MakeDatums(
|
|
tb.Meta().Name.O, // Table
|
|
0, // Non_unique
|
|
"PRIMARY", // Key_name
|
|
1, // Seq_in_index
|
|
pkCol.Name.O, // Column_name
|
|
"utf8_bin", // Colation
|
|
0, // Cardinality
|
|
nil, // Sub_part
|
|
nil, // Packed
|
|
"", // Null
|
|
"BTREE", // Index_type
|
|
"", // Comment
|
|
"", // Index_comment
|
|
)
|
|
e.rows = append(e.rows, &Row{Data: data})
|
|
}
|
|
for _, idx := range tb.Indices() {
|
|
for i, col := range idx.Meta().Columns {
|
|
nonUniq := 1
|
|
if idx.Meta().Unique {
|
|
nonUniq = 0
|
|
}
|
|
var subPart interface{}
|
|
if col.Length != types.UnspecifiedLength {
|
|
subPart = col.Length
|
|
}
|
|
data := types.MakeDatums(
|
|
tb.Meta().Name.O, // Table
|
|
nonUniq, // Non_unique
|
|
idx.Meta().Name.O, // Key_name
|
|
i+1, // Seq_in_index
|
|
col.Name.O, // Column_name
|
|
"utf8_bin", // Colation
|
|
0, // Cardinality
|
|
subPart, // Sub_part
|
|
nil, // Packed
|
|
"YES", // Null
|
|
idx.Meta().Tp.String(), // Index_type
|
|
"", // Comment
|
|
idx.Meta().Comment, // Index_comment
|
|
)
|
|
e.rows = append(e.rows, &Row{Data: data})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// See http://dev.mysql.com/doc/refman/5.7/en/show-character-set.html
|
|
func (e *ShowExec) fetchShowCharset() error {
|
|
descs := charset.GetAllCharsets()
|
|
for _, desc := range descs {
|
|
row := &Row{
|
|
Data: types.MakeDatums(
|
|
desc.Name,
|
|
desc.Desc,
|
|
desc.DefaultCollation,
|
|
desc.Maxlen,
|
|
),
|
|
}
|
|
e.rows = append(e.rows, row)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowVariables() error {
|
|
sessionVars := e.ctx.GetSessionVars()
|
|
for _, v := range variable.SysVars {
|
|
var err error
|
|
var value string
|
|
if !e.GlobalScope {
|
|
// Try to get Session Scope variable value first.
|
|
value, err = varsutil.GetSessionSystemVar(sessionVars, v.Name)
|
|
} else {
|
|
value, err = varsutil.GetGlobalSystemVar(sessionVars, v.Name)
|
|
}
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
row := &Row{Data: types.MakeDatums(v.Name, value)}
|
|
e.rows = append(e.rows, row)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowStatus() error {
|
|
statusVars, err := variable.GetStatusVars()
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
for status, v := range statusVars {
|
|
if e.GlobalScope && v.Scope == variable.ScopeSession {
|
|
continue
|
|
}
|
|
switch v.Value.(type) {
|
|
case []interface{}, nil:
|
|
v.Value = fmt.Sprintf("%v", v.Value)
|
|
}
|
|
value, err := types.ToString(v.Value)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
row := &Row{Data: types.MakeDatums(status, value)}
|
|
e.rows = append(e.rows, row)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowCreateTable() error {
|
|
tb, err := e.getTable()
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
|
|
// TODO: let the result more like MySQL.
|
|
var buf bytes.Buffer
|
|
buf.WriteString(fmt.Sprintf("CREATE TABLE `%s` (\n", tb.Meta().Name.O))
|
|
var pkCol *table.Column
|
|
for i, col := range tb.Cols() {
|
|
buf.WriteString(fmt.Sprintf(" `%s` %s", col.Name.O, col.GetTypeDesc()))
|
|
if mysql.HasAutoIncrementFlag(col.Flag) {
|
|
buf.WriteString(" NOT NULL AUTO_INCREMENT")
|
|
} else {
|
|
if mysql.HasNotNullFlag(col.Flag) {
|
|
buf.WriteString(" NOT NULL")
|
|
}
|
|
if !mysql.HasNoDefaultValueFlag(col.Flag) {
|
|
switch col.DefaultValue {
|
|
case nil:
|
|
if !mysql.HasNotNullFlag(col.Flag) {
|
|
if mysql.HasTimestampFlag(col.Flag) {
|
|
buf.WriteString(" NULL")
|
|
}
|
|
buf.WriteString(" DEFAULT NULL")
|
|
}
|
|
case "CURRENT_TIMESTAMP":
|
|
buf.WriteString(" DEFAULT CURRENT_TIMESTAMP")
|
|
default:
|
|
buf.WriteString(fmt.Sprintf(" DEFAULT '%v'", col.DefaultValue))
|
|
}
|
|
}
|
|
if mysql.HasOnUpdateNowFlag(col.Flag) {
|
|
buf.WriteString(" ON UPDATE CURRENT_TIMESTAMP")
|
|
}
|
|
}
|
|
if len(col.Comment) > 0 {
|
|
buf.WriteString(fmt.Sprintf(" COMMENT '%s'", col.Comment))
|
|
}
|
|
if i != len(tb.Cols())-1 {
|
|
buf.WriteString(",\n")
|
|
}
|
|
if tb.Meta().PKIsHandle && mysql.HasPriKeyFlag(col.Flag) {
|
|
pkCol = col
|
|
}
|
|
}
|
|
|
|
if pkCol != nil {
|
|
// If PKIsHanle, pk info is not in tb.Indices(). We should handle it here.
|
|
buf.WriteString(",\n")
|
|
buf.WriteString(fmt.Sprintf(" PRIMARY KEY (`%s`)", pkCol.Name.O))
|
|
}
|
|
|
|
if len(tb.Indices()) > 0 || len(tb.Meta().ForeignKeys) > 0 {
|
|
buf.WriteString(",\n")
|
|
}
|
|
|
|
for i, idx := range tb.Indices() {
|
|
idxInfo := idx.Meta()
|
|
if idxInfo.Primary {
|
|
buf.WriteString(" PRIMARY KEY ")
|
|
} else if idxInfo.Unique {
|
|
buf.WriteString(fmt.Sprintf(" UNIQUE KEY `%s` ", idxInfo.Name.O))
|
|
} else {
|
|
buf.WriteString(fmt.Sprintf(" KEY `%s` ", idxInfo.Name.O))
|
|
}
|
|
|
|
cols := make([]string, 0, len(idxInfo.Columns))
|
|
for _, c := range idxInfo.Columns {
|
|
cols = append(cols, c.Name.O)
|
|
}
|
|
buf.WriteString(fmt.Sprintf("(`%s`)", strings.Join(cols, "`,`")))
|
|
if i != len(tb.Indices())-1 {
|
|
buf.WriteString(",\n")
|
|
}
|
|
}
|
|
|
|
if len(tb.Indices()) > 0 && len(tb.Meta().ForeignKeys) > 0 {
|
|
buf.WriteString(",\n")
|
|
}
|
|
|
|
firstFK := true
|
|
for _, fk := range tb.Meta().ForeignKeys {
|
|
if fk.State != model.StatePublic {
|
|
continue
|
|
}
|
|
if !firstFK {
|
|
buf.WriteString(",\n")
|
|
}
|
|
firstFK = false
|
|
cols := make([]string, 0, len(fk.Cols))
|
|
for _, c := range fk.Cols {
|
|
cols = append(cols, c.O)
|
|
}
|
|
|
|
refCols := make([]string, 0, len(fk.RefCols))
|
|
for _, c := range fk.Cols {
|
|
refCols = append(refCols, c.O)
|
|
}
|
|
|
|
buf.WriteString(fmt.Sprintf(" CONSTRAINT `%s` FOREIGN KEY (`%s`)", fk.Name.O, strings.Join(cols, "`,`")))
|
|
buf.WriteString(fmt.Sprintf(" REFERENCES `%s` (`%s`)", fk.RefTable.O, strings.Join(refCols, "`,`")))
|
|
|
|
if ast.ReferOptionType(fk.OnDelete) != ast.ReferOptionNoOption {
|
|
buf.WriteString(fmt.Sprintf(" ON DELETE %s", ast.ReferOptionType(fk.OnDelete)))
|
|
}
|
|
|
|
if ast.ReferOptionType(fk.OnUpdate) != ast.ReferOptionNoOption {
|
|
buf.WriteString(fmt.Sprintf(" ON UPDATE %s", ast.ReferOptionType(fk.OnUpdate)))
|
|
}
|
|
}
|
|
buf.WriteString("\n")
|
|
|
|
buf.WriteString(") ENGINE=InnoDB")
|
|
charsetName := tb.Meta().Charset
|
|
if len(charsetName) == 0 {
|
|
charsetName = charset.CharsetUTF8
|
|
}
|
|
collate := tb.Meta().Collate
|
|
if len(collate) == 0 {
|
|
collate = charset.CollationUTF8
|
|
}
|
|
// Because we only support case sensitive utf8_bin collate, we need to explicitly set the default charset and collation
|
|
// to make it work on MySQL server which has default collate utf8_general_ci.
|
|
buf.WriteString(fmt.Sprintf(" DEFAULT CHARSET=%s COLLATE=%s", charsetName, collate))
|
|
|
|
if tb.Meta().AutoIncID > 0 {
|
|
buf.WriteString(fmt.Sprintf(" AUTO_INCREMENT=%d", tb.Meta().AutoIncID))
|
|
}
|
|
|
|
if len(tb.Meta().Comment) > 0 {
|
|
buf.WriteString(fmt.Sprintf(" COMMENT='%s'", tb.Meta().Comment))
|
|
}
|
|
|
|
data := types.MakeDatums(tb.Meta().Name.O, buf.String())
|
|
e.rows = append(e.rows, &Row{Data: data})
|
|
return nil
|
|
}
|
|
|
|
// Compose show create database result.
|
|
func (e *ShowExec) fetchShowCreateDatabase() error {
|
|
db, ok := e.is.SchemaByName(e.DBName)
|
|
if !ok {
|
|
return infoschema.ErrDatabaseNotExists.GenByArgs(e.DBName.O)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
fmt.Fprintf(&buf, "CREATE DATABASE `%s`", db.Name.O)
|
|
if s := db.Charset; len(s) > 0 {
|
|
fmt.Fprintf(&buf, " /* !40100 DEFAULT CHARACTER SET %s */", s)
|
|
}
|
|
|
|
data := types.MakeDatums(db.Name.O, buf.String())
|
|
e.rows = append(e.rows, &Row{Data: data})
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowCollation() error {
|
|
collations := charset.GetCollations()
|
|
for _, v := range collations {
|
|
isDefault := ""
|
|
if v.IsDefault {
|
|
isDefault = "Yes"
|
|
}
|
|
row := &Row{Data: types.MakeDatums(
|
|
v.Name,
|
|
v.CharsetName,
|
|
v.ID,
|
|
isDefault,
|
|
"Yes",
|
|
1,
|
|
)}
|
|
e.rows = append(e.rows, row)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowGrants() error {
|
|
// Get checker
|
|
checker := privilege.GetPrivilegeManager(e.ctx)
|
|
if checker == nil {
|
|
return errors.New("miss privilege checker")
|
|
}
|
|
gs, err := checker.ShowGrants(e.ctx, e.User)
|
|
if err != nil {
|
|
return errors.Trace(err)
|
|
}
|
|
for _, g := range gs {
|
|
data := types.MakeDatums(g)
|
|
e.rows = append(e.rows, &Row{Data: data})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowTriggers() error {
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowProcedureStatus() error {
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) fetchShowWarnings() error {
|
|
warns := e.ctx.GetSessionVars().StmtCtx.GetWarnings()
|
|
for _, warn := range warns {
|
|
datums := make([]types.Datum, 3)
|
|
datums[0] = types.NewStringDatum("Warning")
|
|
switch x := warn.(type) {
|
|
case *terror.Error:
|
|
sqlErr := x.ToSQLError()
|
|
datums[1] = types.NewIntDatum(int64(sqlErr.Code))
|
|
datums[2] = types.NewStringDatum(sqlErr.Message)
|
|
default:
|
|
datums[1] = types.NewIntDatum(int64(mysql.ErrUnknown))
|
|
datums[2] = types.NewStringDatum(warn.Error())
|
|
}
|
|
e.rows = append(e.rows, &Row{Data: datums})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *ShowExec) getTable() (table.Table, error) {
|
|
if e.Table == nil {
|
|
return nil, errors.New("table not found")
|
|
}
|
|
tb, ok := e.is.TableByID(e.Table.TableInfo.ID)
|
|
if !ok {
|
|
return nil, errors.Errorf("table %s not found", e.Table.Name)
|
|
}
|
|
return tb, nil
|
|
}
|
|
|
|
// Close implements the Executor Close interface.
|
|
func (e *ShowExec) Close() error {
|
|
return nil
|
|
}
|