github.com/GnawNom/sqlinternals@v0.0.0-20200413232442-a2dcc5655e0f/mysqlinternals/mysql.go (about)

     1  // sqlinternals for github.com/go-sql-driver/mysql - retrieve column metadata from sql.*Row / sql.*Rows
     2  //
     3  // Copyright 2013 Arne Hormann. All rights reserved.
     4  //
     5  // This Source Code Form is subject to the terms of the Mozilla Public
     6  // License, v. 2.0. If a copy of the MPL was not distributed with this file,
     7  // You can obtain one at http://mozilla.org/MPL/2.0/.
     8  
     9  package mysqlinternals
    10  
    11  import (
    12  	"database/sql"
    13  	"errors"
    14  	"fmt"
    15  	"math/big"
    16  	"reflect"
    17  	"time"
    18  
    19  	"github.com/go-sql-driver/mysql"
    20  )
    21  
    22  // Column represents the column of a MySQL result.
    23  // The methods below postfixed with (*) return information for MySQL internal flags.
    24  // Please note that I can't say if these are trustworthy (esp. IsNotNull), they come directly from MySQL.
    25  // At least for SCHEMA information, MySQL can report false metadata, I don't know if this is different for results.
    26  type Column interface {
    27  
    28  	// mysql.name
    29  
    30  	// Name returns the column name, matching that of a call to Columns() in database/sql
    31  	Name() string
    32  
    33  	// derived from mysqlField.fieldType
    34  
    35  	// MysqlType returns the raw sql type name without parameters and modifiers
    36  	MysqlType() string
    37  	// IsNumber returns true if the column contains numbers (one of integer, decimal or floating point)
    38  	IsNumber() bool
    39  	// IsInteger returns true if the column contains integers
    40  	IsInteger() bool
    41  	// IsFloatingPoint returns true if the column contains floating point numbers
    42  	IsFloatingPoint() bool
    43  	// IsDecimal returns true if the column contains decimal numbers
    44  	IsDecimal() bool
    45  	// IsText returns true if the column contains textual data
    46  	IsText() bool
    47  	// IsBlob returns true if the column contains binary blobs
    48  	IsBlob() bool
    49  	// IsTime returns true if the column contains temporal data
    50  	IsTime() bool
    51  
    52  	// derived from mysqlField.flags
    53  	// TODO: not quite sure about these, add tests and check them.
    54  
    55  	// IsPrimaryKey returns true if the column is marked as part of a primary key (*).
    56  	IsPrimaryKey() bool
    57  	// IsUniqueKey returns true if the column is marked as part of a unique key (*).
    58  	IsUniqueKey() bool
    59  	// IsMultipleKey returns true if the column is marked as part of a regular key (*).
    60  	IsMultipleKey() bool
    61  	// IsNotNull returns true if the column is marked as NOT NULL (*).
    62  	IsNotNull() bool
    63  	// IsUnsigned returns true if the column is marked as UNSIGNED (*).
    64  	IsUnsigned() bool
    65  	// IsZerofill returns true if the column is marked as ZEROFILL (*).
    66  	IsZerofill() bool
    67  	// IsBinary returns true if the column is marked as BINARY (*).
    68  	IsBinary() bool
    69  	// IsAutoIncrement returns true if the column is marked as AUTO_INCREMENT (*).
    70  	IsAutoIncrement() bool
    71  
    72  	// derived from mysqlField.decimals
    73  	Decimals() int
    74  
    75  	// size of a varchar, or display width for a fixed size type
    76  	Length() uint32
    77  
    78  	// derived from mysqlField.fieldType and mysqlField.flags
    79  
    80  	// MysqlParameters returns the category of parameters the SQL type expects in MysqlDeclaration.
    81  	MysqlParameters() parameterType
    82  	// MysqlDeclaration returns a type declaration usable in a CREATE TABLE statement.
    83  	MysqlDeclaration(params ...interface{}) (string, error)
    84  	// ReflectGoType returns the smallest Go type able to represent all possible regular values.
    85  	// The returned types assume a non-NULL value and may cause problems
    86  	// on conversion (e.g. MySQL DATE "0000-00-00", which is not mappable to Go).
    87  	ReflectGoType() (reflect.Type, error)
    88  	// ReflectSqlType returns a Go type able to contain the SQL type, including null values.
    89  	// The returned types may cause problems on conversion
    90  	// (e.g. MySQL DATE "0000-00-00", which is not mappable to Go).
    91  	// The returned type assumes IsNotNull() to be false when forceNullable is set
    92  	// and attempts to return a nullable type (e.g. sql.NullString instead of string).
    93  	ReflectSqlType(forceNullable bool) (reflect.Type, error)
    94  }
    95  
    96  var _ Column = mysqlField{}
    97  
    98  // name of the column
    99  func (f mysqlField) Name() string {
   100  	return f.name
   101  }
   102  
   103  // is a numeric type
   104  func (f mysqlField) IsNumber() bool {
   105  	return f.IsInteger() || f.IsFloatingPoint() || f.IsDecimal()
   106  }
   107  
   108  // is a numeric integer type
   109  func (f mysqlField) IsInteger() bool {
   110  	switch f.fieldType {
   111  	case fieldTypeTiny, fieldTypeShort, fieldTypeInt24, fieldTypeLong, fieldTypeLongLong:
   112  		return true
   113  	}
   114  	return false
   115  }
   116  
   117  // is a numeric binary floating point type
   118  func (f mysqlField) IsFloatingPoint() bool {
   119  	switch f.fieldType {
   120  	case fieldTypeFloat, fieldTypeDouble:
   121  		return true
   122  	}
   123  	return false
   124  }
   125  
   126  // is a numeric decimal type
   127  func (f mysqlField) IsDecimal() bool {
   128  	switch f.fieldType {
   129  	case fieldTypeDecimal, fieldTypeNewDecimal:
   130  		return true
   131  	}
   132  	return false
   133  }
   134  
   135  func (f mysqlField) Length() uint32 {
   136  	return f.length
   137  }
   138  
   139  // is a blob type
   140  func (f mysqlField) IsBlob() bool {
   141  	switch f.fieldType {
   142  	case fieldTypeTinyBLOB, fieldTypeMediumBLOB, fieldTypeBLOB, fieldTypeLongBLOB:
   143  		return true
   144  	}
   145  	return false
   146  }
   147  
   148  // is a textual type
   149  func (f mysqlField) IsText() bool {
   150  	switch f.fieldType {
   151  	case fieldTypeVarChar, fieldTypeVarString, fieldTypeString:
   152  		return true
   153  	}
   154  	return false
   155  }
   156  
   157  // is a temporal type
   158  func (f mysqlField) IsTime() bool {
   159  	switch f.fieldType {
   160  	case fieldTypeYear, fieldTypeDate, fieldTypeNewDate, fieldTypeTime, fieldTypeTimestamp, fieldTypeDateTime:
   161  		return true
   162  	}
   163  	return false
   164  }
   165  
   166  // type name in MySQL (includes "NULL", which may not be used in table definitions)
   167  func (f mysqlField) MysqlType() string {
   168  	return mysqlNameFor(f.fieldType)
   169  }
   170  
   171  // is part of the primary key
   172  func (f mysqlField) IsPrimaryKey() bool {
   173  	return f.flags&flagPriKey == flagPriKey
   174  }
   175  
   176  // is part of a unique key
   177  func (f mysqlField) IsUniqueKey() bool {
   178  	return f.flags&flagUniqueKey == flagUniqueKey
   179  }
   180  
   181  // is part of a nonunique key
   182  func (f mysqlField) IsMultipleKey() bool {
   183  	return f.flags&flagMultipleKey == flagMultipleKey
   184  }
   185  
   186  // has NOT NULL attribute set
   187  func (f mysqlField) IsNotNull() bool {
   188  	return f.flags&flagNotNULL == flagNotNULL
   189  }
   190  
   191  // has UNSIGNED attribute set
   192  func (f mysqlField) IsUnsigned() bool {
   193  	return f.flags&flagUnsigned == flagUnsigned
   194  }
   195  
   196  // has ZEROFILL attribute set
   197  func (f mysqlField) IsZerofill() bool {
   198  	return f.flags&flagZeroFill == flagZeroFill
   199  }
   200  
   201  // has BINARY attribute set
   202  func (f mysqlField) IsBinary() bool {
   203  	return f.flags&flagBinary == flagBinary
   204  }
   205  
   206  // has AUTO_INCREMENT attribute set
   207  func (f mysqlField) IsAutoIncrement() bool {
   208  	return f.flags&flagAutoIncrement == flagAutoIncrement
   209  }
   210  
   211  func (f mysqlField) Decimals() int {
   212  	return int(f.decimals)
   213  }
   214  
   215  const ( // base for reflection
   216  	reflect_uint8   = uint8(0)
   217  	reflect_uint16  = uint16(0)
   218  	reflect_uint32  = uint32(0)
   219  	reflect_uint64  = uint64(0)
   220  	reflect_int8    = int8(0)
   221  	reflect_int16   = int16(0)
   222  	reflect_int32   = int32(0)
   223  	reflect_int64   = int64(0)
   224  	reflect_float32 = float32(0)
   225  	reflect_float64 = float64(0)
   226  	reflect_string  = ""
   227  	// possible indicators for NULL, SET, ENUM, GEOMETRY?
   228  	// reflect_empty   = struct{}{}
   229  	// reflect_many    = []interface{}{}
   230  )
   231  
   232  var ( // reflect.Types
   233  	// non-null types
   234  	typeUint8   = reflect.TypeOf(reflect_uint8)
   235  	typeUint16  = reflect.TypeOf(reflect_uint16)
   236  	typeUint32  = reflect.TypeOf(reflect_uint32)
   237  	typeUint64  = reflect.TypeOf(reflect_uint64)
   238  	typeInt8    = reflect.TypeOf(reflect_int8)
   239  	typeInt16   = reflect.TypeOf(reflect_int16)
   240  	typeInt32   = reflect.TypeOf(reflect_int32)
   241  	typeInt64   = reflect.TypeOf(reflect_int64)
   242  	typeFloat32 = reflect.TypeOf(reflect_float32)
   243  	typeFloat64 = reflect.TypeOf(reflect_float64)
   244  	typeString  = reflect.TypeOf(reflect_string)
   245  	typeBigint  = reflect.TypeOf(big.NewInt(0))
   246  	typeBools   = reflect.TypeOf([]bool{})
   247  	typeBytes   = reflect.TypeOf([]byte{})
   248  	typeTime    = reflect.TypeOf(time.Time{})
   249  	// nullable types
   250  	typeNullInt64   = reflect.TypeOf(sql.NullInt64{})
   251  	typeNullFloat64 = reflect.TypeOf(sql.NullFloat64{})
   252  	typeNullString  = reflect.TypeOf(sql.NullString{})
   253  	typeNullTime    = reflect.TypeOf(mysql.NullTime{})
   254  	// typeNullBool doesn't match in MySQL, boolean is (unsigned?) tinyint(1),
   255  	// it may have more than 2 states
   256  	//typeNullBool = reflect.TypeOf(sql.NullBool{})
   257  )
   258  
   259  // retrieve the best matching reflect.Type for the mysql field.
   260  // Returns an error if no matching type exists.
   261  func (f mysqlField) ReflectGoType() (reflect.Type, error) {
   262  	if f.IsUnsigned() {
   263  		switch f.fieldType {
   264  		case fieldTypeTiny:
   265  			return typeUint8, nil
   266  		case fieldTypeShort:
   267  			return typeUint16, nil
   268  		case fieldTypeInt24, fieldTypeLong:
   269  			return typeUint32, nil
   270  		case fieldTypeLongLong:
   271  			return typeUint64, nil
   272  		}
   273  		// unsigned non-integer types fall through
   274  	}
   275  	switch f.fieldType {
   276  	case fieldTypeTiny:
   277  		return typeInt8, nil
   278  	case fieldTypeShort:
   279  		return typeInt16, nil
   280  	case fieldTypeInt24, fieldTypeLong:
   281  		return typeInt32, nil
   282  	case fieldTypeLongLong:
   283  		return typeInt64, nil
   284  	case fieldTypeFloat:
   285  		return typeFloat32, nil
   286  	case fieldTypeDouble:
   287  		return typeFloat64, nil
   288  	case fieldTypeDecimal, fieldTypeNewDecimal:
   289  		return typeBigint, nil
   290  	case fieldTypeYear, fieldTypeDate, fieldTypeNewDate, fieldTypeTime, fieldTypeTimestamp, fieldTypeDateTime:
   291  		return typeTime, nil
   292  	case fieldTypeBit:
   293  		return typeBools, nil
   294  	case fieldTypeVarChar, fieldTypeVarString, fieldTypeString:
   295  		return typeString, nil
   296  	case fieldTypeTinyBLOB, fieldTypeMediumBLOB, fieldTypeBLOB, fieldTypeLongBLOB,
   297  		fieldTypeJSON:
   298  		return typeBytes, nil
   299  	case fieldTypeEnum, fieldTypeSet, fieldTypeGeometry, fieldTypeNULL:
   300  		return nil, errorTypeMismatch(f.fieldType)
   301  	}
   302  	return nil, errors.New("unknown mysql type")
   303  }
   304  
   305  // retrieve the best matching reflect.Type for the mysql field.
   306  // Returns an error if no matching type exists.
   307  func (f mysqlField) ReflectSqlType(forceNullable bool) (reflect.Type, error) {
   308  	if forceNullable || !f.IsNotNull() {
   309  		switch {
   310  		case f.IsInteger():
   311  			return typeNullInt64, nil
   312  		case f.IsFloatingPoint():
   313  			return typeNullFloat64, nil
   314  		case f.IsText():
   315  			return typeNullString, nil
   316  		case f.IsTime():
   317  			return typeNullTime, nil
   318  		case f.IsBlob():
   319  			return typeBytes, nil // []byte can be nil on its own
   320  		}
   321  		// All other types are not nullable in Go right now
   322  		return nil, errorTypeMismatch(f.fieldType)
   323  	}
   324  	return f.ReflectGoType()
   325  }
   326  
   327  type errorTypeMismatch uint8
   328  
   329  func (e errorTypeMismatch) Error() string {
   330  	return "no matching go type for " + mysqlNameFor(fieldType(e))
   331  }
   332  
   333  func mysqlNameFor(fieldType fieldType) string {
   334  	switch fieldType {
   335  	// --- integer ---
   336  	case fieldTypeTiny:
   337  		return "TINYINT"
   338  	case fieldTypeShort:
   339  		return "SMALLINT"
   340  	case fieldTypeInt24, fieldTypeLong:
   341  		return "INT"
   342  	case fieldTypeLongLong:
   343  		return "BIGINT"
   344  	// --- floating point ---
   345  	case fieldTypeFloat:
   346  		return "FLOAT"
   347  	case fieldTypeDouble:
   348  		return "DOUBLE"
   349  	// --- decimal ---
   350  	case fieldTypeDecimal, fieldTypeNewDecimal:
   351  		return "DECIMAL"
   352  	// --- date & time ---
   353  	case fieldTypeYear:
   354  		return "YEAR"
   355  	case fieldTypeDate, fieldTypeNewDate:
   356  		return "DATE"
   357  	case fieldTypeTime:
   358  		return "TIME"
   359  	case fieldTypeTimestamp:
   360  		return "TIMESTAMP"
   361  	case fieldTypeDateTime:
   362  		return "DATETIME"
   363  	// --- null ---
   364  	case fieldTypeNULL:
   365  		return "NULL"
   366  	// --- bit ---
   367  	case fieldTypeBit:
   368  		return "BIT"
   369  	// --- string ---
   370  	case fieldTypeVarChar, fieldTypeVarString:
   371  		return "VARCHAR"
   372  	case fieldTypeString:
   373  		return "CHAR"
   374  	// --- enum ---
   375  	case fieldTypeEnum:
   376  		return "ENUM"
   377  	// --- set ---
   378  	case fieldTypeSet:
   379  		return "SET"
   380  	// --- blob ---
   381  	case fieldTypeTinyBLOB:
   382  		return "TINY BLOB"
   383  	case fieldTypeMediumBLOB:
   384  		return "MEDIUM BLOB"
   385  	case fieldTypeBLOB:
   386  		return "BLOB"
   387  	case fieldTypeLongBLOB:
   388  		return "LONG BLOB"
   389  	// --- geometry ---
   390  	case fieldTypeGeometry:
   391  		return "GEOMETRY"
   392  	// --- JSON ---
   393  	case fieldTypeJSON:
   394  		return "JSON"
   395  	}
   396  	return ""
   397  }
   398  
   399  type parameterType uint
   400  
   401  const (
   402  	// unknown type, no information about parameter requirements
   403  	ParamUnknown parameterType = iota
   404  	// requires no parameters in MySQL declaration
   405  	ParamNone
   406  	// requires no parameters or length (int > 0) in MySQL declaration
   407  	ParamMayLength
   408  	// requires length (int > 0) in MySQL declaration
   409  	ParamMustLength
   410  	// requires no parameters or length (int > 0) and decimals (int >= 0) in MySQL declaration
   411  	// OBSOLETE since decimals are contained in mysqlField...
   412  	// ParamMayLengthAndDecimals
   413  	_
   414  	// requires no parameters or length (int > 0) or length and decimals (int >= 0) in MySQL declaration
   415  	// OBSOLETE since decimals are contained in mysqlField...
   416  	// ParamMayLengthMayDecimals
   417  	_
   418  	// requires valid values as parameters in MySQL declaration
   419  	ParamValues
   420  )
   421  
   422  // retrieve information about parameters used in MysqlDeclaration
   423  func (f mysqlField) MysqlParameters() parameterType {
   424  	switch f.fieldType {
   425  	case // date types, *BLOB and GEOMETRY declarations have no parameters
   426  		fieldTypeYear, fieldTypeDate, fieldTypeNewDate,
   427  		fieldTypeTinyBLOB, fieldTypeMediumBLOB, fieldTypeBLOB, fieldTypeLongBLOB,
   428  		fieldTypeGeometry, fieldTypeJSON,
   429  		// time types use decimals: microseconds
   430  		fieldTypeTime, fieldTypeTimestamp, fieldTypeDateTime:
   431  		return ParamNone
   432  	case // BIT, *INT* and CHAR declarations have one optional parameter (length)
   433  		fieldTypeBit,
   434  		fieldTypeTiny, fieldTypeShort, fieldTypeInt24, fieldTypeLong, fieldTypeLongLong,
   435  		fieldTypeString,
   436  		// DECIMAL and NUMERIC declarations have one optional parameter (length) and may use decimals
   437  		fieldTypeDecimal, fieldTypeNewDecimal,
   438  		// REAL, FLOAT and DOUBLE declarations have one optional parameter (length, will also use decimals when length is given)
   439  		fieldTypeFloat, fieldTypeDouble:
   440  		return ParamMayLength
   441  	case // VARCHAR and VARBINARY declarations have one mandatory parameter (length)
   442  		fieldTypeVarChar, fieldTypeVarString:
   443  		return ParamMustLength
   444  	case // ENUM and SET declarations have multiple parameters
   445  		fieldTypeEnum, fieldTypeSet:
   446  		return ParamValues
   447  	}
   448  	return ParamUnknown
   449  }
   450  
   451  type paramErr string
   452  
   453  func (p paramErr) Error() string {
   454  	return string(p)
   455  }
   456  
   457  // mysql type declaration
   458  // The declaration includes the type and size and the attributes "NOT NULL", "ZEROFILL" and "BINARY".
   459  // It does not include the name, character sets, collations, default value, keys or the attribute auto_increment.
   460  // For BIT, all INT types, CHAR and BINARY types, args is optional and may be one int: length.
   461  // For VARCHAR and VARBINARY types, args must be one int: length.
   462  // For DECIMAL and NUMERIC types, it may be none or one int: length.
   463  // For DATETIME, TIME, TIMESTAMP, decimals is used for microseconds.
   464  // For FLOAT, DOUBLE and REAL floating point types, it is optional and, when given, must be two ints: length and decimals.
   465  // For SETs and ENUMs, it specifies the possible values.
   466  // For all other types, args must be empty.
   467  func (f mysqlField) MysqlDeclaration(args ...interface{}) (string, error) {
   468  	const (
   469  		unsigned = " UNSIGNED"
   470  		notNull  = " NOT NULL"
   471  		zerofill = " ZEROFILL"
   472  		binary   = " BINARY"
   473  		// errors
   474  		errNil        = paramErr("can't create declaration for NULL")
   475  		errUnknown    = paramErr("parameter error, unknown")
   476  		errNone       = paramErr("parameter error, must be none")
   477  		errMayLength  = paramErr("parameter error, must be none or one int (length)")
   478  		errMustLength = paramErr("parameter error, must be one int (length)")
   479  		errEnumOrSet  = paramErr("parameter error, must be at least one entry")
   480  	)
   481  	// fail fast if we can't provide a declaration
   482  	if f.fieldType == fieldTypeNULL {
   483  		return "", errNil
   484  	}
   485  	var param, us, nn, zf, bin string
   486  	if f.IsNotNull() {
   487  		// any type may be "NOT NULL"
   488  		nn = notNull
   489  	}
   490  	switch f.fieldType {
   491  	case fieldTypeFloat, fieldTypeDouble,
   492  		fieldTypeDecimal, fieldTypeNewDecimal:
   493  		if len(args) == 1 {
   494  			param = fmt.Sprintf("(%d,%d)", args[0], f.decimals)
   495  		}
   496  		fallthrough
   497  	case // numeric types may be unsigned or zerofill
   498  		fieldTypeTiny, fieldTypeShort, fieldTypeInt24, fieldTypeLong, fieldTypeLongLong:
   499  		if f.IsUnsigned() {
   500  			us = unsigned
   501  		}
   502  		if f.IsZerofill() {
   503  			zf = zerofill
   504  		}
   505  	case fieldTypeBit:
   506  		if len(args) != 1 {
   507  			return "", errMustLength
   508  		}
   509  		param = fmt.Sprintf("(%d)", args[0])
   510  	case fieldTypeYear, fieldTypeDate, fieldTypeNewDate,
   511  		fieldTypeTinyBLOB, fieldTypeMediumBLOB, fieldTypeBLOB, fieldTypeLongBLOB,
   512  		fieldTypeGeometry, fieldTypeJSON:
   513  		// nothing to be done for these types
   514  	case // only string types may be binary
   515  		fieldTypeVarChar, fieldTypeVarString:
   516  		if f.IsBinary() {
   517  			bin = binary
   518  		}
   519  		if len(args) != 1 {
   520  			return "", errMustLength
   521  		}
   522  		param = fmt.Sprintf("(%d)", args[0])
   523  	case fieldTypeString:
   524  		if f.IsBinary() {
   525  			bin = binary
   526  		}
   527  		if len(args) == 1 {
   528  			param = fmt.Sprintf("(%d)", args[0])
   529  		}
   530  	case fieldTypeTime, fieldTypeTimestamp, fieldTypeDateTime:
   531  		if f.decimals > 0 {
   532  			param = fmt.Sprintf("(%d)", f.decimals)
   533  		}
   534  
   535  	case fieldTypeEnum, fieldTypeSet:
   536  		if len(args) == 0 {
   537  			return "", errEnumOrSet
   538  		}
   539  		param = fmt.Sprintf("(%v)", args...)
   540  	default:
   541  		return "", errUnknown
   542  	}
   543  	return mysqlNameFor(f.fieldType) + param + bin + us + zf + nn, nil
   544  }