2015-01-20 07:15:48 -06:00
package migrator
2015-01-19 03:44:16 -06:00
2017-03-28 07:34:53 -05:00
import (
2022-02-15 10:54:27 -06:00
"database/sql"
2020-11-19 07:47:17 -06:00
"errors"
2020-10-16 02:46:14 -05:00
"fmt"
2017-03-28 07:34:53 -05:00
"strconv"
"strings"
2018-05-10 09:54:21 -05:00
2018-09-27 05:07:43 -05:00
"github.com/VividCortex/mysqlerr"
"github.com/go-sql-driver/mysql"
2022-02-15 10:54:27 -06:00
"github.com/golang-migrate/migrate/v4/database"
2020-04-01 08:57:21 -05:00
"xorm.io/xorm"
2017-03-28 07:34:53 -05:00
)
2015-01-19 03:44:16 -06:00
2020-11-10 23:21:08 -06:00
type MySQLDialect struct {
2015-01-19 03:44:16 -06:00
BaseDialect
}
2020-10-19 13:06:12 -05:00
func NewMysqlDialect ( engine * xorm . Engine ) Dialect {
2020-11-10 23:21:08 -06:00
d := MySQLDialect { }
2015-01-19 03:44:16 -06:00
d . BaseDialect . dialect = & d
2018-05-10 09:54:21 -05:00
d . BaseDialect . engine = engine
2020-11-10 23:21:08 -06:00
d . BaseDialect . driverName = MySQL
2015-01-19 03:44:16 -06:00
return & d
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) SupportEngine ( ) bool {
2015-01-20 07:44:37 -06:00
return true
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) Quote ( name string ) string {
2015-01-19 03:44:16 -06:00
return "`" + name + "`"
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) AutoIncrStr ( ) string {
2015-01-19 03:44:16 -06:00
return "AUTO_INCREMENT"
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) BooleanStr ( value bool ) string {
2016-10-25 07:52:20 -05:00
if value {
2016-12-13 21:15:35 -06:00
return "1"
}
return "0"
2016-09-23 01:07:14 -05:00
}
2022-11-01 13:24:32 -05:00
func ( db * MySQLDialect ) BatchSize ( ) int {
return 1000
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) SQLType ( c * Column ) string {
2015-01-19 03:44:16 -06:00
var res string
switch c . Type {
case DB_Bool :
res = DB_TinyInt
c . Length = 1
case DB_Serial :
c . IsAutoIncrement = true
c . IsPrimaryKey = true
c . Nullable = false
res = DB_Int
case DB_BigSerial :
c . IsAutoIncrement = true
c . IsPrimaryKey = true
c . Nullable = false
res = DB_BigInt
case DB_Bytea :
res = DB_Blob
case DB_TimeStampz :
res = DB_Char
c . Length = 64
case DB_NVarchar :
res = DB_Varchar
default :
res = c . Type
}
2018-04-27 15:14:36 -05:00
var hasLen1 = ( c . Length > 0 )
var hasLen2 = ( c . Length2 > 0 )
2015-01-19 03:44:16 -06:00
if res == DB_BigInt && ! hasLen1 && ! hasLen2 {
c . Length = 20
hasLen1 = true
}
if hasLen2 {
res += "(" + strconv . Itoa ( c . Length ) + "," + strconv . Itoa ( c . Length2 ) + ")"
} else if hasLen1 {
res += "(" + strconv . Itoa ( c . Length ) + ")"
}
2017-03-28 07:34:53 -05:00
switch c . Type {
case DB_Char , DB_Varchar , DB_NVarchar , DB_TinyText , DB_Text , DB_MediumText , DB_LongText :
2022-11-04 16:30:22 -05:00
if c . IsLatin {
res += " CHARACTER SET latin1 COLLATE latin1_bin"
} else {
res += " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
}
2017-03-28 07:34:53 -05:00
}
2015-01-19 03:44:16 -06:00
return res
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) UpdateTableSQL ( tableName string , columns [ ] * Column ) string {
2017-03-28 07:34:53 -05:00
var statements = [ ] string { }
statements = append ( statements , "DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" )
for _ , col := range columns {
statements = append ( statements , "MODIFY " + col . StringNoPk ( db ) )
}
return "ALTER TABLE " + db . Quote ( tableName ) + " " + strings . Join ( statements , ", " ) + ";"
}
2018-05-10 09:54:21 -05:00
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) IndexCheckSQL ( tableName , indexName string ) ( string , [ ] interface { } ) {
2018-12-18 14:47:45 -06:00
args := [ ] interface { } { tableName , indexName }
sql := "SELECT 1 FROM " + db . Quote ( "INFORMATION_SCHEMA" ) + "." + db . Quote ( "STATISTICS" ) + " WHERE " + db . Quote ( "TABLE_SCHEMA" ) + " = DATABASE() AND " + db . Quote ( "TABLE_NAME" ) + "=? AND " + db . Quote ( "INDEX_NAME" ) + "=?"
return sql , args
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) ColumnCheckSQL ( tableName , columnName string ) ( string , [ ] interface { } ) {
2018-12-18 16:02:08 -06:00
args := [ ] interface { } { tableName , columnName }
sql := "SELECT 1 FROM " + db . Quote ( "INFORMATION_SCHEMA" ) + "." + db . Quote ( "COLUMNS" ) + " WHERE " + db . Quote ( "TABLE_SCHEMA" ) + " = DATABASE() AND " + db . Quote ( "TABLE_NAME" ) + "=? AND " + db . Quote ( "COLUMN_NAME" ) + "=?"
return sql , args
}
2022-06-03 10:42:08 -05:00
func ( db * MySQLDialect ) RenameColumn ( table Table , column * Column , newName string ) string {
2022-05-23 06:13:55 -05:00
quote := db . dialect . Quote
2022-06-03 10:42:08 -05:00
return fmt . Sprintf (
"ALTER TABLE %s CHANGE %s %s %s" ,
quote ( table . Name ) , quote ( column . Name ) , quote ( newName ) , db . SQLType ( column ) ,
)
2022-05-23 06:13:55 -05:00
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) CleanDB ( ) error {
2020-07-10 09:09:21 -05:00
tables , err := db . engine . DBMetas ( )
if err != nil {
return err
}
2018-05-10 09:54:21 -05:00
sess := db . engine . NewSession ( )
defer sess . Close ( )
for _ , table := range tables {
2021-01-07 04:36:13 -06:00
switch table . Name {
default :
if _ , err := sess . Exec ( "set foreign_key_checks = 0" ) ; err != nil {
2022-06-03 02:24:24 -05:00
return fmt . Errorf ( "%v: %w" , "failed to disable foreign key checks" , err )
2021-01-07 04:36:13 -06:00
}
if _ , err := sess . Exec ( "drop table " + table . Name + " ;" ) ; err != nil {
2022-06-06 15:30:31 -05:00
return fmt . Errorf ( "failed to delete table %q: %w" , table . Name , err )
2021-01-07 04:36:13 -06:00
}
if _ , err := sess . Exec ( "set foreign_key_checks = 1" ) ; err != nil {
2022-06-03 02:24:24 -05:00
return fmt . Errorf ( "%v: %w" , "failed to disable foreign key checks" , err )
2021-01-07 04:36:13 -06:00
}
2018-05-10 09:54:21 -05:00
}
}
return nil
}
2018-09-27 05:07:43 -05:00
2020-10-16 02:46:14 -05:00
// TruncateDBTables truncates all the tables.
// A special case is the dashboard_acl table where we keep the default permissions.
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) TruncateDBTables ( ) error {
2020-10-16 02:46:14 -05:00
tables , err := db . engine . DBMetas ( )
if err != nil {
return err
}
sess := db . engine . NewSession ( )
defer sess . Close ( )
for _ , table := range tables {
switch table . Name {
2021-08-25 08:11:22 -05:00
case "migration_log" :
continue
2020-10-16 02:46:14 -05:00
case "dashboard_acl" :
// keep default dashboard permissions
if _ , err := sess . Exec ( fmt . Sprintf ( "DELETE FROM %v WHERE dashboard_id != -1 AND org_id != -1;" , db . Quote ( table . Name ) ) ) ; err != nil {
2022-06-06 15:30:31 -05:00
return fmt . Errorf ( "failed to truncate table %q: %w" , table . Name , err )
2020-10-16 02:46:14 -05:00
}
if _ , err := sess . Exec ( fmt . Sprintf ( "ALTER TABLE %v AUTO_INCREMENT = 3;" , db . Quote ( table . Name ) ) ) ; err != nil {
2022-06-06 15:30:31 -05:00
return fmt . Errorf ( "failed to reset table %q: %w" , table . Name , err )
2020-10-16 02:46:14 -05:00
}
default :
if _ , err := sess . Exec ( fmt . Sprintf ( "TRUNCATE TABLE %v;" , db . Quote ( table . Name ) ) ) ; err != nil {
2022-06-06 15:30:31 -05:00
return fmt . Errorf ( "failed to truncate table %q: %w" , table . Name , err )
2020-10-16 02:46:14 -05:00
}
}
}
return nil
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) isThisError ( err error , errcode uint16 ) bool {
2020-11-19 07:47:17 -06:00
var driverErr * mysql . MySQLError
if errors . As ( err , & driverErr ) {
2019-06-13 08:36:09 -05:00
if driverErr . Number == errcode {
2018-09-27 05:07:43 -05:00
return true
}
}
return false
}
2019-06-13 08:36:09 -05:00
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) IsUniqueConstraintViolation ( err error ) bool {
2019-06-13 08:36:09 -05:00
return db . isThisError ( err , mysqlerr . ER_DUP_ENTRY )
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) ErrorMessage ( err error ) string {
2020-11-19 07:47:17 -06:00
var driverErr * mysql . MySQLError
if errors . As ( err , & driverErr ) {
2020-04-20 08:48:38 -05:00
return driverErr . Message
}
return ""
}
2020-11-10 23:21:08 -06:00
func ( db * MySQLDialect ) IsDeadlock ( err error ) bool {
2019-06-13 08:36:09 -05:00
return db . isThisError ( err , mysqlerr . ER_LOCK_DEADLOCK )
}
2021-01-18 12:57:17 -06:00
2022-07-19 03:42:48 -05:00
// UpsertSQL returns the upsert sql statement for MySQL dialect
2021-01-18 12:57:17 -06:00
func ( db * MySQLDialect ) UpsertSQL ( tableName string , keyCols , updateCols [ ] string ) string {
2022-07-19 03:42:48 -05:00
q , _ := db . UpsertMultipleSQL ( tableName , keyCols , updateCols , 1 )
return q
}
func ( db * MySQLDialect ) UpsertMultipleSQL ( tableName string , keyCols , updateCols [ ] string , count int ) ( string , error ) {
if count < 1 {
return "" , fmt . Errorf ( "upsert statement must have count >= 1. Got %v" , count )
}
2021-01-18 12:57:17 -06:00
columnsStr := strings . Builder { }
colPlaceHoldersStr := strings . Builder { }
setStr := strings . Builder { }
separator := ", "
for i , c := range updateCols {
if i == len ( updateCols ) - 1 {
separator = ""
}
columnsStr . WriteString ( fmt . Sprintf ( "%s%s" , db . Quote ( c ) , separator ) )
colPlaceHoldersStr . WriteString ( fmt . Sprintf ( "?%s" , separator ) )
setStr . WriteString ( fmt . Sprintf ( "%s=VALUES(%s)%s" , db . Quote ( c ) , db . Quote ( c ) , separator ) )
}
2022-07-19 03:42:48 -05:00
valuesStr := strings . Builder { }
separator = ", "
colPlaceHolders := colPlaceHoldersStr . String ( )
for i := 0 ; i < count ; i ++ {
if i == count - 1 {
separator = ""
}
valuesStr . WriteString ( fmt . Sprintf ( "(%s)%s" , colPlaceHolders , separator ) )
}
s := fmt . Sprintf ( ` INSERT INTO %s (%s) VALUES %s ON DUPLICATE KEY UPDATE %s ` ,
2021-01-18 12:57:17 -06:00
tableName ,
columnsStr . String ( ) ,
2022-07-19 03:42:48 -05:00
valuesStr . String ( ) ,
2021-01-18 12:57:17 -06:00
setStr . String ( ) ,
)
2022-07-19 03:42:48 -05:00
return s , nil
2021-01-18 12:57:17 -06:00
}
2022-02-15 10:54:27 -06:00
func ( db * MySQLDialect ) Lock ( cfg LockCfg ) error {
query := "SELECT GET_LOCK(?, ?)"
var success sql . NullBool
lockName , err := db . getLockName ( )
if err != nil {
return fmt . Errorf ( "failed to generate lock name: %w" , err )
}
// trying to obtain the lock with the specific name
// the lock is exclusive per session and is released explicitly by executing RELEASE_LOCK() or implicitly when the session terminates
// it returns 1 if the lock was obtained successfully,
// 0 if the attempt timed out (for example, because another client has previously locked the name),
// or NULL if an error occurred
// starting from MySQL 5.7 it is even possible for a given session to acquire multiple locks for the same name
// however other sessions cannot acquire a lock with that name until the acquiring session releases all its locks for the name.
_ , err = cfg . Session . SQL ( query , lockName , cfg . Timeout ) . Get ( & success )
if err != nil {
return err
}
if ! success . Valid || ! success . Bool {
return ErrLockDB
}
return nil
}
func ( db * MySQLDialect ) Unlock ( cfg LockCfg ) error {
query := "SELECT RELEASE_LOCK(?)"
var success sql . NullBool
lockName , err := db . getLockName ( )
if err != nil {
return fmt . Errorf ( "failed to generate lock name: %w" , err )
}
// trying to release the lock with the specific name
// it returns 1 if the lock was released,
// 0 if the lock was not established by this thread (in which case the lock is not released),
// and NULL if the named lock did not exist (it was never obtained by a call to GET_LOCK() or if it has previously been released)
_ , err = cfg . Session . SQL ( query , lockName ) . Get ( & success )
if err != nil {
return err
}
if ! success . Valid || ! success . Bool {
return ErrReleaseLockDB
}
return nil
}
func ( db * MySQLDialect ) getLockName ( ) ( string , error ) {
cfg , err := mysql . ParseDSN ( db . engine . DataSourceName ( ) )
if err != nil {
return "" , err
}
s , err := database . GenerateAdvisoryLockId ( cfg . DBName )
if err != nil {
return "" , fmt . Errorf ( "failed to generate advisory lock key: %w" , err )
}
return s , nil
}