sqlboiler/bdb/drivers/mysql.go

357 lines
9.5 KiB
Go

package drivers
import (
"database/sql"
"fmt"
"strconv"
"strings"
"github.com/go-sql-driver/mysql"
"github.com/pkg/errors"
"github.com/vattle/sqlboiler/bdb"
)
// TinyintAsBool is a global that is set from main.go if a user specifies
// this flag when generating. This flag only applies to MySQL so we're using
// a global instead, to avoid breaking the interface. If TinyintAsBool is true
// then tinyint(1) will be mapped in your generated structs to bool opposed to int8.
var TinyintAsBool bool
// MySQLDriver holds the database connection string and a handle
// to the database connection.
type MySQLDriver struct {
connStr string
dbConn *sql.DB
}
// NewMySQLDriver takes the database connection details as parameters and
// returns a pointer to a MySQLDriver object. Note that it is required to
// call MySQLDriver.Open() and MySQLDriver.Close() to open and close
// the database connection once an object has been obtained.
func NewMySQLDriver(user, pass, dbname, host string, port int, sslmode string) *MySQLDriver {
driver := MySQLDriver{
connStr: MySQLBuildQueryString(user, pass, dbname, host, port, sslmode),
}
return &driver
}
// MySQLBuildQueryString builds a query string for MySQL.
func MySQLBuildQueryString(user, pass, dbname, host string, port int, sslmode string) string {
var config mysql.Config
config.User = user
if len(pass) != 0 {
config.Passwd = pass
}
config.DBName = dbname
config.Net = "tcp"
config.Addr = host
if port == 0 {
port = 3306
}
config.Addr += ":" + strconv.Itoa(port)
config.TLSConfig = sslmode
// MySQL is a bad, and by default reads date/datetime into a []byte
// instead of a time.Time. Tell it to stop being a bad.
config.ParseTime = true
return config.FormatDSN()
}
// Open opens the database connection using the connection string
func (m *MySQLDriver) Open() error {
var err error
m.dbConn, err = sql.Open("mysql", m.connStr)
if err != nil {
return err
}
return nil
}
// Close closes the database connection
func (m *MySQLDriver) Close() {
m.dbConn.Close()
}
// UseLastInsertID returns false for postgres
func (m *MySQLDriver) UseLastInsertID() bool {
return true
}
// UseTopClause returns false to indicate MySQL doesnt support SQL TOP clause
func (m *MySQLDriver) UseTopClause() bool {
return false
}
// TableNames connects to the postgres database and
// retrieves all table names from the information_schema where the
// table schema is public.
func (m *MySQLDriver) TableNames(schema string, whitelist, blacklist []string) ([]string, error) {
var names []string
query := fmt.Sprintf(`select table_name from information_schema.tables where table_schema = ? and table_type = 'BASE TABLE'`)
args := []interface{}{schema}
if len(whitelist) > 0 {
query += fmt.Sprintf(" and table_name in (%s);", strings.Repeat(",?", len(whitelist))[1:])
for _, w := range whitelist {
args = append(args, w)
}
} else if len(blacklist) > 0 {
query += fmt.Sprintf(" and table_name not in (%s);", strings.Repeat(",?", len(blacklist))[1:])
for _, b := range blacklist {
args = append(args, b)
}
}
rows, err := m.dbConn.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
names = append(names, name)
}
return names, nil
}
// Columns takes a table name and attempts to retrieve the table information
// from the database information_schema.columns. It retrieves the column names
// and column types and returns those as a []Column after TranslateColumnType()
// converts the SQL types to Go types, for example: "varchar" to "string"
func (m *MySQLDriver) Columns(schema, tableName string) ([]bdb.Column, error) {
var columns []bdb.Column
rows, err := m.dbConn.Query(`
select
c.column_name,
c.column_type,
if(c.data_type = 'enum', c.column_type, c.data_type),
if(extra = 'auto_increment','auto_increment', c.column_default),
c.is_nullable = 'YES',
exists (
select c.column_name
from information_schema.table_constraints tc
inner join information_schema.key_column_usage kcu
on tc.constraint_name = kcu.constraint_name and tc.table_name = kcu.table_name and tc.table_schema = kcu.table_schema
where c.column_name = kcu.column_name and tc.table_name = c.table_name and
(tc.constraint_type = 'PRIMARY KEY' or tc.constraint_type = 'UNIQUE') and
(select count(*) from information_schema.key_column_usage where table_schema = kcu.table_schema and table_name = tc.table_name and constraint_name = tc.constraint_name) = 1
) as is_unique
from information_schema.columns as c
where table_name = ? and table_schema = ?;
`, tableName, schema)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var colName, colType, colFullType string
var nullable, unique bool
var defaultValue *string
if err := rows.Scan(&colName, &colFullType, &colType, &defaultValue, &nullable, &unique); err != nil {
return nil, errors.Wrapf(err, "unable to scan for table %s", tableName)
}
column := bdb.Column{
Name: colName,
FullDBType: colFullType, // example: tinyint(1) instead of tinyint
DBType: colType,
Nullable: nullable,
Unique: unique,
}
if defaultValue != nil && *defaultValue != "NULL" {
column.Default = *defaultValue
}
columns = append(columns, column)
}
return columns, nil
}
// PrimaryKeyInfo looks up the primary key for a table.
func (m *MySQLDriver) PrimaryKeyInfo(schema, tableName string) (*bdb.PrimaryKey, error) {
pkey := &bdb.PrimaryKey{}
var err error
query := `
select tc.constraint_name
from information_schema.table_constraints as tc
where tc.table_name = ? and tc.constraint_type = 'PRIMARY KEY' and tc.table_schema = ?;`
row := m.dbConn.QueryRow(query, tableName, schema)
if err = row.Scan(&pkey.Name); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
queryColumns := `
select kcu.column_name
from information_schema.key_column_usage as kcu
where table_name = ? and constraint_name = ? and table_schema = ?;`
var rows *sql.Rows
if rows, err = m.dbConn.Query(queryColumns, tableName, pkey.Name, schema); err != nil {
return nil, err
}
defer rows.Close()
var columns []string
for rows.Next() {
var column string
err = rows.Scan(&column)
if err != nil {
return nil, err
}
columns = append(columns, column)
}
if err = rows.Err(); err != nil {
return nil, err
}
pkey.Columns = columns
return pkey, nil
}
// ForeignKeyInfo retrieves the foreign keys for a given table name.
func (m *MySQLDriver) ForeignKeyInfo(schema, tableName string) ([]bdb.ForeignKey, error) {
var fkeys []bdb.ForeignKey
query := `
select constraint_name, table_name, column_name, referenced_table_name, referenced_column_name
from information_schema.key_column_usage
where table_schema = ? and referenced_table_schema = ? and table_name = ?
`
var rows *sql.Rows
var err error
if rows, err = m.dbConn.Query(query, schema, schema, tableName); err != nil {
return nil, err
}
for rows.Next() {
var fkey bdb.ForeignKey
var sourceTable string
fkey.Table = tableName
err = rows.Scan(&fkey.Name, &sourceTable, &fkey.Column, &fkey.ForeignTable, &fkey.ForeignColumn)
if err != nil {
return nil, err
}
fkeys = append(fkeys, fkey)
}
if err = rows.Err(); err != nil {
return nil, err
}
return fkeys, nil
}
// TranslateColumnType converts postgres database types to Go types, for example
// "varchar" to "string" and "bigint" to "int64". It returns this parsed data
// as a Column object.
func (m *MySQLDriver) TranslateColumnType(c bdb.Column) bdb.Column {
if c.Nullable {
switch c.DBType {
case "tinyint":
// map tinyint(1) to bool if TinyintAsBool is true
if TinyintAsBool && c.FullDBType == "tinyint(1)" {
c.Type = "null.Bool"
} else {
c.Type = "null.Int8"
}
case "smallint":
c.Type = "null.Int16"
case "mediumint":
c.Type = "null.Int32"
case "int", "integer":
c.Type = "null.Int"
case "bigint":
c.Type = "null.Int64"
case "float":
c.Type = "null.Float32"
case "double", "double precision", "real":
c.Type = "null.Float64"
case "boolean", "bool":
c.Type = "null.Bool"
case "date", "datetime", "timestamp", "time":
c.Type = "null.Time"
case "binary", "varbinary", "tinyblob", "blob", "mediumblob", "longblob":
c.Type = "null.Bytes"
case "json":
c.Type = "types.JSON"
default:
c.Type = "null.String"
}
} else {
switch c.DBType {
case "tinyint":
// map tinyint(1) to bool if TinyintAsBool is true
if TinyintAsBool && c.FullDBType == "tinyint(1)" {
c.Type = "bool"
} else {
c.Type = "int8"
}
case "smallint":
c.Type = "int16"
case "mediumint":
c.Type = "int32"
case "int", "integer":
c.Type = "int"
case "bigint":
c.Type = "int64"
case "float":
c.Type = "float32"
case "double", "double precision", "real":
c.Type = "float64"
case "boolean", "bool":
c.Type = "bool"
case "date", "datetime", "timestamp", "time":
c.Type = "time.Time"
case "binary", "varbinary", "tinyblob", "blob", "mediumblob", "longblob":
c.Type = "[]byte"
case "json":
c.Type = "types.JSON"
default:
c.Type = "string"
}
}
return c
}
// RightQuote is the quoting character for the right side of the identifier
func (m *MySQLDriver) RightQuote() byte {
return '`'
}
// LeftQuote is the quoting character for the left side of the identifier
func (m *MySQLDriver) LeftQuote() byte {
return '`'
}
// IndexPlaceholders returns false to indicate MySQL doesnt support indexed placeholders
func (m *MySQLDriver) IndexPlaceholders() bool {
return false
}