// Copyright 2015 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package dialects

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"

	"code.forgejo.org/xorm/xorm/core"
	"code.forgejo.org/xorm/xorm/schemas"
)

var (
	mysqlReservedWords = map[string]bool{
		"ADD":               true,
		"ALL":               true,
		"ALTER":             true,
		"ANALYZE":           true,
		"AND":               true,
		"AS":                true,
		"ASC":               true,
		"ASENSITIVE":        true,
		"BEFORE":            true,
		"BETWEEN":           true,
		"BIGINT":            true,
		"BINARY":            true,
		"BLOB":              true,
		"BOTH":              true,
		"BY":                true,
		"CALL":              true,
		"CASCADE":           true,
		"CASE":              true,
		"CHAIN":             true,
		"CHANGE":            true,
		"CHAR":              true,
		"CHARACTER":         true,
		"CHECK":             true,
		"COLLATE":           true,
		"COLUMN":            true,
		"CONDITION":         true,
		"CONNECTION":        true,
		"CONSTRAINT":        true,
		"CONTINUE":          true,
		"CONVERT":           true,
		"CREATE":            true,
		"CROSS":             true,
		"CURRENT_DATE":      true,
		"CURRENT_TIME":      true,
		"CURRENT_TIMESTAMP": true,
		"CURRENT_USER":      true,
		"CURSOR":            true,
		"DATABASE":          true,
		"DATABASES":         true,
		"DAY_HOUR":          true,
		"DAY_MICROSECOND":   true,
		"DAY_MINUTE":        true,
		"DAY_SECOND":        true,
		"DEC":               true,
		"DECIMAL":           true,
		"DECLARE":           true,
		"DEFAULT":           true,
		"DELAYED":           true,
		"DELETE":            true,
		"DESC":              true,
		"DESCRIBE":          true,
		"DETERMINISTIC":     true,
		"DISTINCT":          true,
		"DISTINCTROW":       true,
		"DIV":               true,
		"DOUBLE":            true,
		"DROP":              true,
		"DUAL":              true,
		"EACH":              true,
		"ELSE":              true,
		"ELSEIF":            true,
		"ENCLOSED":          true,
		"ESCAPED":           true,
		"EXISTS":            true,
		"EXIT":              true,
		"EXPLAIN":           true,
		"FALSE":             true,
		"FETCH":             true,
		"FLOAT":             true,
		"FLOAT4":            true,
		"FLOAT8":            true,
		"FOR":               true,
		"FORCE":             true,
		"FOREIGN":           true,
		"FROM":              true,
		"FULLTEXT":          true,
		"GOTO":              true,
		"GRANT":             true,
		"GROUP":             true,
		"HAVING":            true,
		"HIGH_PRIORITY":     true,
		"HOUR_MICROSECOND":  true,
		"HOUR_MINUTE":       true,
		"HOUR_SECOND":       true,
		"IF":                true,
		"IGNORE":            true,
		"IN":                true, "INDEX": true,
		"INFILE": true, "INNER": true, "INOUT": true,
		"INSENSITIVE": true, "INSERT": true, "INT": true,
		"INT1": true, "INT2": true, "INT3": true,
		"INT4": true, "INT8": true, "INTEGER": true,
		"INTERVAL": true, "INTO": true, "IS": true,
		"ITERATE": true, "JOIN": true, "KEY": true,
		"KEYS": true, "KILL": true, "LABEL": true,
		"LEADING": true, "LEAVE": true, "LEFT": true,
		"LIKE": true, "LIMIT": true, "LINEAR": true,
		"LINES": true, "LOAD": true, "LOCALTIME": true,
		"LOCALTIMESTAMP": true, "LOCK": true, "LONG": true,
		"LONGBLOB": true, "LONGTEXT": true, "LOOP": true,
		"LOW_PRIORITY": true, "MATCH": true, "MEDIUMBLOB": true,
		"MEDIUMINT": true, "MEDIUMTEXT": true, "MIDDLEINT": true,
		"MINUTE_MICROSECOND": true, "MINUTE_SECOND": true, "MOD": true,
		"MODIFIES": true, "NATURAL": true, "NOT": true,
		"NO_WRITE_TO_BINLOG": true, "NULL": true, "NUMERIC": true,
		"ON	OPTIMIZE": true, "OPTION": true,
		"OPTIONALLY": true, "OR": true, "ORDER": true,
		"OUT": true, "OUTER": true, "OUTFILE": true,
		"PRECISION": true, "PRIMARY": true, "PROCEDURE": true,
		"PURGE": true, "RAID0": true, "RANGE": true,
		"RANK": true,
		"READ": true, "READS": true, "REAL": true,
		"REFERENCES": true, "REGEXP": true, "RELEASE": true,
		"RENAME": true, "REPEAT": true, "REPLACE": true,
		"REQUIRE": true, "RESTRICT": true, "RETURN": true,
		"REVOKE": true, "RIGHT": true, "RLIKE": true,
		"SCHEMA": true, "SCHEMAS": true, "SECOND_MICROSECOND": true,
		"SELECT": true, "SENSITIVE": true, "SEPARATOR": true,
		"SET": true, "SHOW": true, "SMALLINT": true,
		"SPATIAL": true, "SPECIFIC": true, "SQL": true,
		"SQLEXCEPTION": true, "SQLSTATE": true, "SQLWARNING": true,
		"SQL_BIG_RESULT": true, "SQL_CALC_FOUND_ROWS": true, "SQL_SMALL_RESULT": true,
		"SSL": true, "STARTING": true, "STRAIGHT_JOIN": true,
		"TABLE": true, "TERMINATED": true, "THEN": true,
		"TINYBLOB": true, "TINYINT": true, "TINYTEXT": true,
		"TO": true, "TRAILING": true, "TRIGGER": true,
		"TRUE": true, "UNDO": true, "UNION": true,
		"UNIQUE": true, "UNLOCK": true, "UNSIGNED": true,
		"UPDATE": true, "USAGE": true, "USE": true,
		"USING": true, "UTC_DATE": true, "UTC_TIME": true,
		"UTC_TIMESTAMP": true, "VALUES": true, "VARBINARY": true,
		"VARCHAR":      true,
		"VARCHARACTER": true,
		"VARYING":      true,
		"WHEN":         true,
		"WHERE":        true,
		"WHILE":        true,
		"WITH":         true,
		"WRITE":        true,
		"X509":         true,
		"XOR":          true,
		"YEAR_MONTH":   true,
		"ZEROFILL":     true,
	}

	mysqlQuoter = schemas.Quoter{
		Prefix:     '`',
		Suffix:     '`',
		IsReserved: schemas.AlwaysReserve,
	}
)

type mysql struct {
	Base
	rowFormat string
}

func (db *mysql) Init(uri *URI) error {
	db.quoter = mysqlQuoter
	return db.Base.Init(db, uri)
}

var mysqlColAliases = map[string]string{
	"numeric": "decimal",
}

// Alias returns a alias of column
func (db *mysql) Alias(col string) string {
	v, ok := mysqlColAliases[strings.ToLower(col)]
	if ok {
		return v
	}
	return col
}

func (db *mysql) Version(ctx context.Context, queryer core.Queryer) (*schemas.Version, error) {
	rows, err := queryer.QueryContext(ctx, "SELECT @@VERSION")
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var version string
	if !rows.Next() {
		if rows.Err() != nil {
			return nil, rows.Err()
		}
		return nil, errors.New("unknow version")
	}

	if err := rows.Scan(&version); err != nil {
		return nil, err
	}

	fields := strings.Split(version, "-")
	if len(fields) == 3 && fields[1] == "TiDB" {
		// 5.7.25-TiDB-v3.0.3
		return &schemas.Version{
			Number:  strings.TrimPrefix(fields[2], "v"),
			Level:   fields[0],
			Edition: fields[1],
		}, nil
	}

	var edition string
	if len(fields) == 2 {
		edition = fields[1]
	}

	return &schemas.Version{
		Number:  fields[0],
		Edition: edition,
	}, nil
}

func (db *mysql) Features() *DialectFeatures {
	return &DialectFeatures{
		AutoincrMode: IncrAutoincrMode,
	}
}

func (db *mysql) SetParams(params map[string]string) {
	rowFormat, ok := params["rowFormat"]
	if ok {
		t := strings.ToUpper(rowFormat)
		switch t {
		case "COMPACT":
			fallthrough
		case "REDUNDANT":
			fallthrough
		case "DYNAMIC":
			fallthrough
		case "COMPRESSED":
			db.rowFormat = t
		}
	}
}

func (db *mysql) SQLType(c *schemas.Column) string {
	var res string
	var isUnsigned bool
	switch t := c.SQLType.Name; t {
	case schemas.Bool:
		res = schemas.TinyInt
		c.Length = 1
	case schemas.Serial:
		c.IsAutoIncrement = true
		c.IsPrimaryKey = true
		c.Nullable = false
		res = schemas.Int
	case schemas.BigSerial:
		c.IsAutoIncrement = true
		c.IsPrimaryKey = true
		c.Nullable = false
		res = schemas.BigInt
	case schemas.Bytea:
		res = schemas.Blob
	case schemas.TimeStampz:
		res = schemas.Char
		c.Length = 64
	case schemas.Enum: // mysql enum
		res = schemas.Enum
		res += "("
		var opts strings.Builder
		for v := range c.EnumOptions {
			fmt.Fprintf(&opts, ",'%v'", v)
		}
		res += strings.TrimLeft(opts.String(), ",")
		res += ")"
	case schemas.Set: // mysql set
		res = schemas.Set
		res += "("
		var opts strings.Builder
		for v := range c.SetOptions {
			fmt.Fprintf(&opts, ",'%v'", v)
		}
		res += strings.TrimLeft(opts.String(), ",")
		res += ")"
	case schemas.NVarchar:
		res = schemas.Varchar
	case schemas.Uuid:
		res = schemas.Varchar
		c.Length = 40
	case schemas.Json:
		res = schemas.Text
	case schemas.UnsignedInt:
		res = schemas.Int
		isUnsigned = true
	case schemas.UnsignedBigInt:
		res = schemas.BigInt
		isUnsigned = true
	case schemas.UnsignedMediumInt:
		res = schemas.MediumInt
		isUnsigned = true
	case schemas.UnsignedSmallInt:
		res = schemas.SmallInt
		isUnsigned = true
	case schemas.UnsignedTinyInt:
		res = schemas.TinyInt
		isUnsigned = true
	case schemas.UnsignedFloat:
		res = schemas.Float
		isUnsigned = true
	default:
		res = t
	}

	hasLen1 := c.Length > 0
	hasLen2 := c.Length2 > 0

	if res == schemas.BigInt && !hasLen1 && !hasLen2 {
		c.Length = 20
		hasLen1 = true
	}

	if hasLen2 {
		res += "(" + strconv.FormatInt(c.Length, 10) + "," + strconv.FormatInt(c.Length2, 10) + ")"
	} else if hasLen1 {
		res += "(" + strconv.FormatInt(c.Length, 10) + ")"
	}

	if isUnsigned {
		res += " UNSIGNED"
	}

	return res
}

func (db *mysql) ColumnTypeKind(t string) int {
	switch strings.ToUpper(t) {
	case "DATETIME":
		return schemas.TIME_TYPE
	case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT", "ENUM", "SET":
		return schemas.TEXT_TYPE
	case "BIGINT", "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "FLOAT", "REAL", "DOUBLE PRECISION", "DECIMAL", "NUMERIC", "BIT":
		return schemas.NUMERIC_TYPE
	case "BINARY", "VARBINARY", "TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB":
		return schemas.BLOB_TYPE
	default:
		return schemas.UNKNOW_TYPE
	}
}

func (db *mysql) IsReserved(name string) bool {
	_, ok := mysqlReservedWords[strings.ToUpper(name)]
	return ok
}

func (db *mysql) AutoIncrStr() string {
	return "AUTO_INCREMENT"
}

func (db *mysql) IndexCheckSQL(tableName, idxName string) (string, []any) {
	args := []any{db.uri.DBName, tableName, idxName}
	sql := "SELECT `INDEX_NAME` FROM `INFORMATION_SCHEMA`.`STATISTICS`"
	sql += " WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ? AND `INDEX_NAME`=?"
	return sql, args
}

func (db *mysql) IsTableExist(ctx context.Context, queryer core.Queryer, tableName string) (bool, error) {
	sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?"
	return db.HasRecords(ctx, queryer, sql, db.uri.DBName, tableName)
}

func (db *mysql) AddColumnSQL(tableName string, col *schemas.Column) string {
	quoter := db.dialect.Quoter()
	s, _ := ColumnString(db, col, true, true)
	var b strings.Builder
	b.WriteString("ALTER TABLE ")
	quoter.QuoteTo(&b, tableName)
	b.WriteString(" ADD ")
	b.WriteString(s)
	if len(col.Comment) > 0 {
		b.WriteString(" COMMENT '")
		b.WriteString(col.Comment)
		b.WriteString("'")
	}
	return b.String()
}

// ModifyColumnSQL returns a SQL to modify SQL
func (db *mysql) ModifyColumnSQL(tableName string, col *schemas.Column) string {
	s, _ := ColumnString(db.dialect, col, false, true)
	if col.Comment != "" {
		s += fmt.Sprintf(" COMMENT '%s'", col.Comment)
	}
	return fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s", db.quoter.Quote(tableName), s)
}

func (db *mysql) GetColumns(ctx context.Context, queryer core.Queryer, tableName string) ([]string, map[string]*schemas.Column, error) {
	args := []any{db.uri.DBName, tableName}
	alreadyQuoted := "(INSTR(VERSION(), 'maria') > 0 && " +
		"(SUBSTRING_INDEX(VERSION(), '.', 1) > 10 || " +
		"(SUBSTRING_INDEX(VERSION(), '.', 1) = 10 && " +
		"(SUBSTRING_INDEX(SUBSTRING(VERSION(), 4), '.', 1) > 2 || " +
		"(SUBSTRING_INDEX(SUBSTRING(VERSION(), 4), '.', 1) = 2 && " +
		"SUBSTRING_INDEX(SUBSTRING(VERSION(), 6), '-', 1) >= 7)))))"
	s := "SELECT `COLUMN_NAME`, `IS_NULLABLE`, `COLUMN_DEFAULT`, `COLUMN_TYPE`," +
		" `COLUMN_KEY`, `EXTRA`, `COLUMN_COMMENT`, `CHARACTER_MAXIMUM_LENGTH`, " +
		alreadyQuoted + " AS NEEDS_QUOTE, `COLLATION_NAME` " +
		"FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?" +
		" ORDER BY `COLUMNS`.ORDINAL_POSITION ASC"

	rows, err := queryer.QueryContext(ctx, s, args...)
	if err != nil {
		return nil, nil, err
	}
	defer rows.Close()

	cols := make(map[string]*schemas.Column)
	colSeq := make([]string, 0)
	for rows.Next() {
		col := new(schemas.Column)
		col.Indexes = make(map[string]int)

		var columnName, nullableStr, colType, colKey, extra, comment string
		var alreadyQuoted, isUnsigned bool
		var colDefault, maxLength, collation *string
		err = rows.Scan(&columnName, &nullableStr, &colDefault, &colType, &colKey, &extra, &comment, &maxLength, &alreadyQuoted, &collation)
		if err != nil {
			return nil, nil, err
		}
		col.Name = strings.Trim(columnName, "` ")
		col.Comment = comment
		if nullableStr == "YES" {
			col.Nullable = true
		}

		if colDefault != nil && (!alreadyQuoted || *colDefault != "NULL") {
			col.Default = *colDefault
			col.DefaultIsEmpty = false
		} else {
			col.DefaultIsEmpty = true
		}
		if collation != nil {
			col.Collation = *collation
		}

		fields := strings.Fields(colType)
		if len(fields) == 2 && fields[1] == "unsigned" {
			isUnsigned = true
		}
		colType = fields[0]
		cts := strings.Split(colType, "(")
		colName := cts[0]
		// Remove the /* mariadb-5.3 */ suffix from coltypes
		colName = strings.TrimSuffix(colName, "/* mariadb-5.3 */")
		colType = strings.ToUpper(colName)
		var len1, len2 int64
		if len(cts) == 2 {
			idx := strings.Index(cts[1], ")")
			if colType == schemas.Enum && cts[1][0] == '\'' { // enum
				options := strings.Split(cts[1][0:idx], ",")
				col.EnumOptions = make(map[string]int)
				for k, v := range options {
					v = strings.TrimSpace(v)
					v = strings.Trim(v, "'")
					col.EnumOptions[v] = k
				}
			} else if colType == schemas.Set && cts[1][0] == '\'' {
				options := strings.Split(cts[1][0:idx], ",")
				col.SetOptions = make(map[string]int)
				for k, v := range options {
					v = strings.TrimSpace(v)
					v = strings.Trim(v, "'")
					col.SetOptions[v] = k
				}
			} else {
				lens := strings.Split(cts[1][0:idx], ",")
				len1, err = strconv.ParseInt(strings.TrimSpace(lens[0]), 10, 64)
				if err != nil {
					return nil, nil, err
				}
				if len(lens) == 2 {
					len2, err = strconv.ParseInt(lens[1], 10, 64)
					if err != nil {
						return nil, nil, err
					}
				}
			}
		} else {
			switch colType {
			case "MEDIUMTEXT", "LONGTEXT", "TEXT":
				len1, err = strconv.ParseInt(*maxLength, 10, 64)
				if err != nil {
					return nil, nil, err
				}
			}
		}
		if isUnsigned {
			colType = "UNSIGNED " + colType
		}
		col.Length = len1
		col.Length2 = len2
		if _, ok := schemas.SqlTypes[colType]; !ok {
			return nil, nil, fmt.Errorf("unknown colType %v", colType)
		}
		col.SQLType = schemas.SQLType{Name: colType, DefaultLength: len1, DefaultLength2: len2}

		if colKey == "PRI" {
			col.IsPrimaryKey = true
		}
		// if colKey == "UNI" {
		// col.is
		// }

		if extra == "auto_increment" {
			col.IsAutoIncrement = true
		}

		if !col.DefaultIsEmpty {
			if !alreadyQuoted && col.SQLType.IsText() {
				col.Default = "'" + col.Default + "'"
			} else if col.SQLType.IsTime() && !alreadyQuoted && col.Default != "CURRENT_TIMESTAMP" {
				col.Default = "'" + col.Default + "'"
			}
		}
		cols[col.Name] = col
		colSeq = append(colSeq, col.Name)
	}
	if rows.Err() != nil {
		return nil, nil, rows.Err()
	}
	return colSeq, cols, nil
}

func (db *mysql) GetTables(ctx context.Context, queryer core.Queryer) ([]*schemas.Table, error) {
	args := []any{db.uri.DBName}
	s := "SELECT `TABLE_NAME`, `ENGINE`, `AUTO_INCREMENT`, `TABLE_COMMENT`, `TABLE_COLLATION` from " +
		"`INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? AND (`ENGINE`='MyISAM' OR `ENGINE` = 'InnoDB' OR `ENGINE` = 'TokuDB')"

	rows, err := queryer.QueryContext(ctx, s, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	tables := make([]*schemas.Table, 0)
	for rows.Next() {
		table := schemas.NewEmptyTable()
		var name, engine, collation string
		var autoIncr, comment *string
		err = rows.Scan(&name, &engine, &autoIncr, &comment, &collation)
		if err != nil {
			return nil, err
		}

		table.Name = name
		if comment != nil {
			table.Comment = *comment
		}
		table.StoreEngine = engine
		table.Collation = collation
		tables = append(tables, table)
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}
	return tables, nil
}

func (db *mysql) SetQuotePolicy(quotePolicy QuotePolicy) {
	switch quotePolicy {
	case QuotePolicyNone:
		q := mysqlQuoter
		q.IsReserved = schemas.AlwaysNoReserve
		db.quoter = q
	case QuotePolicyReserved:
		q := mysqlQuoter
		q.IsReserved = db.IsReserved
		db.quoter = q
	case QuotePolicyAlways:
		fallthrough
	default:
		db.quoter = mysqlQuoter
	}
}

func (db *mysql) GetIndexes(ctx context.Context, queryer core.Queryer, tableName string) (map[string]*schemas.Index, error) {
	args := []any{db.uri.DBName, tableName}
	s := `SELECT
		INDEX_NAME,
		NON_UNIQUE,
		COLUMN_NAME,
		EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
			WHERE kcu.TABLE_NAME = s.TABLE_NAME
			AND kcu.TABLE_SCHEMA = s.TABLE_SCHEMA
			AND (s.INDEX_NAME = CONCAT('IDX_', kcu.CONSTRAINT_NAME) OR s.INDEX_NAME = kcu.CONSTRAINT_NAME)
			AND kcu.REFERENCED_TABLE_SCHEMA IS NOT NULL
			AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
			AND kcu.REFERENCED_COLUMN_NAME IS NOT NULL
		) AS is_fk_index
	FROM
		INFORMATION_SCHEMA.STATISTICS s
	WHERE
		TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY SEQ_IN_INDEX
	`

	rows, err := queryer.QueryContext(ctx, s, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	indexes := make(map[string]*schemas.Index)
	for rows.Next() {
		var indexType int
		var indexName, colName, nonUnique string
		var isForeignKeyIndex bool
		err = rows.Scan(&indexName, &nonUnique, &colName, &isForeignKeyIndex)
		if err != nil {
			return nil, err
		}

		if indexName == "PRIMARY" {
			continue
		}

		if nonUnique == "YES" || nonUnique == "1" {
			indexType = schemas.IndexType
		} else {
			indexType = schemas.UniqueType
		}

		colName = strings.Trim(colName, "` ")
		var isRegular bool
		if strings.HasPrefix(indexName, "IDX_"+tableName) || strings.HasPrefix(indexName, "UQE_"+tableName) {
			indexName = indexName[5+len(tableName):]
			isRegular = true
		}

		var index *schemas.Index
		var ok bool
		if index, ok = indexes[indexName]; !ok {
			index = new(schemas.Index)
			index.IsRegular = isRegular
			index.IsForeignKey = isForeignKeyIndex
			index.Type = indexType
			index.Name = indexName
			indexes[indexName] = index
		}
		index.AddColumn(colName)
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}
	return indexes, nil
}

func (db *mysql) CreateTableSQL(table *schemas.Table, tableName string) (string, bool, error) {
	if tableName == "" {
		tableName = table.Name
	}

	quoter := db.dialect.Quoter()
	var b strings.Builder
	b.WriteString("CREATE TABLE IF NOT EXISTS ")
	quoter.QuoteTo(&b, tableName)
	b.WriteString(" (")

	for i, colName := range table.ColumnsSeq() {
		col := table.GetColumn(colName)
		s, _ := ColumnString(db.dialect, col, col.IsPrimaryKey && len(table.PrimaryKeys) == 1, true)
		b.WriteString(s)

		if len(col.Comment) > 0 {
			b.WriteString(" COMMENT '")
			b.WriteString(col.Comment)
			b.WriteString("'")
		}

		if i != len(table.ColumnsSeq())-1 {
			b.WriteString(", ")
		}
	}

	if len(table.PrimaryKeys) > 1 {
		b.WriteString(", PRIMARY KEY (")
		b.WriteString(quoter.Join(table.PrimaryKeys, ","))
		b.WriteString(")")
	}

	for _, fk := range table.ForeignKeys {
		b.WriteString(", ")
		b.WriteString(db.dialect.ForeignKeyConstraintSQL(fk))
	}

	b.WriteString(")")

	if table.StoreEngine != "" {
		b.WriteString(" ENGINE=")
		b.WriteString(table.StoreEngine)
	}

	charset := table.Charset
	if len(charset) == 0 {
		charset = db.URI().Charset
	}
	if len(charset) != 0 {
		b.WriteString(" DEFAULT CHARSET ")
		b.WriteString(charset)
	}

	if db.rowFormat != "" {
		b.WriteString(" ROW_FORMAT=")
		b.WriteString(db.rowFormat)
	}

	if table.Comment != "" {
		b.WriteString(" COMMENT='")
		b.WriteString(table.Comment)
		b.WriteString("'")
	}

	return b.String(), true, nil
}

func (db *mysql) Filters() []Filter {
	return []Filter{}
}

func (db *mysql) GetForeignKeys(ctx context.Context, queryer core.Queryer, tableName string) ([]schemas.ForeignKey, error) {
	args := []any{tableName}
	s := `SELECT
    kcu.CONSTRAINT_NAME as fk_name,
    kcu.COLUMN_NAME AS source_field_name,
    kcu.REFERENCED_TABLE_NAME AS target_table_name,
    kcu.REFERENCED_COLUMN_NAME AS target_field_name
FROM
    INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
    INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
        ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
        AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
WHERE
    kcu.TABLE_NAME = ?
    AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
	AND rc.DELETE_RULE IN ('RESTRICT', 'NO ACTION')
	AND rc.UPDATE_RULE IN ('RESTRICT', 'NO ACTION')
	AND kcu.ORDINAL_POSITION = 1
	AND (
	    SELECT COUNT(*)
	    FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu2
	    WHERE kcu2.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
	    AND kcu2.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
	) = 1
	AND kcu.TABLE_SCHEMA = kcu.REFERENCED_TABLE_SCHEMA
`

	rows, err := queryer.QueryContext(ctx, s, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	foreignKeys := make([]schemas.ForeignKey, 0)

	for rows.Next() {
		var constraintName, sourceFieldName, targetTableName, targetFieldName string
		err = rows.Scan(&constraintName, &sourceFieldName, &targetTableName, &targetFieldName)
		if err != nil {
			return nil, err
		}

		foreignKeys = append(foreignKeys, schemas.ForeignKey{
			Name:            constraintName,
			SourceFieldName: sourceFieldName,
			TargetTableName: targetTableName,
			TargetFieldName: targetFieldName,
		})
	}
	if rows.Err() != nil {
		return nil, rows.Err()
	}

	return foreignKeys, nil
}

func (db *mysql) GetAlterTableSQL(originalTable *schemas.Table, tableName string, modifications ...schemas.TableModification) ([]AlterTableOp, error) {
	if len(modifications) == 0 {
		return []AlterTableOp{}, nil
	}

	// MySQL DBs don't appear to support both dropping and adding constraints in the same ALTER TABLE command; there are
	// no errors, I couldn't find any documentation, but the DROP operation just gets ignored.  As a workaround we'll
	// split the modifications by their type and do both separately.
	addAlterations := []schemas.TableModification{}
	dropAlterations := []schemas.TableModification{}
	for _, modification := range modifications {
		switch m := modification.(type) {
		case schemas.AddForeignKey:
			addAlterations = append(addAlterations, m)
		case schemas.DropForeignKey:
			dropAlterations = append(dropAlterations, m)
		default:
			return nil, fmt.Errorf("unknown modification type: %T", modification)
		}
	}

	statements := make([]AlterTableOp, 0, len(modifications))

	dropStatements, err := db.Base.GetAlterTableSQL(originalTable, tableName, dropAlterations...)
	if err != nil {
		return nil, err
	}
	statements = append(statements, dropStatements...)

	addStatements, err := db.Base.GetAlterTableSQL(originalTable, tableName, addAlterations...)
	if err != nil {
		return nil, err
	}
	statements = append(statements, addStatements...)

	return statements, nil
}

type mysqlDriver struct {
	baseDriver
}

func (p *mysqlDriver) Features() *DriverFeatures {
	return &DriverFeatures{
		SupportReturnInsertedID: true,
	}
}

func (p *mysqlDriver) Parse(driverName, dataSourceName string) (*URI, string, error) {
	dsnPattern := regexp.MustCompile(
		`^(?:(?P<user>.*?)(?::(?P<passwd>.*))?@)?` + // [user[:password]@]
			`(?:(?P<net>[^\(]*)(?:\((?P<addr>[^\)]*)\))?)?` + // [net[(addr)]]
			`\/(?P<dbname>.*?)` + // /dbname
			`(?:\?(?P<params>[^\?]*))?$`, // [?param1=value1&paramN=valueN]
	)
	matches := dsnPattern.FindStringSubmatch(dataSourceName)
	// tlsConfigRegister := make(map[string]*tls.Config)
	names := dsnPattern.SubexpNames()

	uri := &URI{DBType: schemas.MYSQL}

	for i, match := range matches {
		switch names[i] {
		case "dbname":
			uri.DBName = match
		case "params":
			if len(match) > 0 {
				kvs := strings.SplitSeq(match, "&")
				for kv := range kvs {
					splits := strings.Split(kv, "=")
					if len(splits) == 2 {
						if splits[0] == "charset" {
							uri.Charset = splits[1]
						}
					}
				}
			}
		}
	}
	return uri, dataSourceName, nil
}

func (p *mysqlDriver) GenScanResult(colType string) (any, error) {
	colType = strings.ReplaceAll(colType, "UNSIGNED ", "")
	switch colType {
	case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT", "ENUM", "SET", "JSON":
		var s sql.NullString
		return &s, nil
	case "BIGINT":
		var s sql.NullInt64
		return &s, nil
	case "TINYINT", "SMALLINT", "MEDIUMINT", "INT":
		var s sql.NullInt32
		return &s, nil
	case "FLOAT", "REAL", "DOUBLE PRECISION", "DOUBLE":
		var s sql.NullFloat64
		return &s, nil
	case "DECIMAL", "NUMERIC":
		var s sql.NullString
		return &s, nil
	case "DATETIME", "TIMESTAMP":
		var s sql.NullTime
		return &s, nil
	case "BIT":
		var s sql.RawBytes
		return &s, nil
	case "BINARY", "VARBINARY", "TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB":
		var r sql.RawBytes
		return &r, nil
	default:
		var r sql.RawBytes
		return &r, nil
	}
}

func (p *mysqlDriver) StandardizeError(err error) error {
	return mysqlStandardizeError(err)
}

// Extract the error code field from a variety of MySQL error types through reflection, without linking those libraries.
// MySQL drivers inspected at this time don't have any methods defined on them which could be put into an interface, so
// we need to use reflection instead.
func getMySQLErrorCode(err error) (uint, bool) {
	if err == nil {
		return 0, false
	}

	v := reflect.ValueOf(err)
	if v.Kind() == reflect.Pointer {
		v = v.Elem()
	}

	if v.Kind() != reflect.Struct {
		return 0, false
	}

	// github.com/go-sql-driver/mysql has a `Number uint16` field on `MySQLError`.
	codeField := v.FieldByName("Number")
	if !codeField.IsValid() {
		return 0, false
	}
	if !codeField.CanUint() {
		return 0, false
	}

	return uint(codeField.Uint()), true
}

func mysqlStandardizeError(err error) error {
	if err == nil {
		return nil
	}

	errorNumber, ok := getMySQLErrorCode(err)
	if ok {
		switch errorNumber {
		case 1062:
			return fmt.Errorf("%w: %w", ErrUniqueConstraintViolation, err)
		case 1451: // Cannot delete or update a parent row
			fallthrough
		case 1452: // Cannot add or update a child row
			return fmt.Errorf("%w: %w", ErrForeignKeyViolation, err)
		case 1020, 1205, 1213:
			// 1205 -- Lock wait timeout exceeded -- error message also includes "try restarting transaction" thus very
			// close to the behavior how we generally want to handle deadlocks. On a retry, locks are more likely to be
			// available. NOTE: There's no automated test that covers this case.
			//
			// 1213 -- deadlock found when trying to get lock.
			//
			// 1020 -- Record has changed since last read in table -- technically this isn't a deadlock, but reflects
			// that a transaction in "repeatable read" isolation mode cannot provide a repeatable read.  This is lumped
			// into ErrDeadlock because the application response will likely be the same -- retry the transaction (or
			// error).  NOTE: There's no automated test that covers this case.
			return fmt.Errorf("%w: %w", ErrDeadlock, err)
		}
	}

	return err
}
