Coverage Summary for Class: MysqlSupport (com.kotlinorm.database.mysql)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| MysqlSupport |
44%
(11/25)
|
20.7%
(39/188)
|
27.8%
(55/198)
|
38.1%
(783/2053)
|
| MysqlSupport$getTableIndexes$lambda$1$$inlined$sortedBy$1 |
0%
(0/1)
|
|
| MysqlSupport$WhenMappings |
|
| Total |
42.3%
(11/26)
|
20.7%
(39/188)
|
27.8%
(55/198)
|
38.1%
(783/2053)
|
/**
* Copyright 2022-2025 kronos-orm
*
* 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,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kotlinorm.database.mysql
import com.kotlinorm.beans.dsl.Field
import com.kotlinorm.beans.dsl.FunctionField
import com.kotlinorm.beans.dsl.KTableIndex
import com.kotlinorm.beans.task.KronosAtomicQueryTask
import com.kotlinorm.database.ConflictResolver
import com.kotlinorm.database.SqlManager
import com.kotlinorm.database.SqlManager.columnCreateDefSql
import com.kotlinorm.database.SqlManager.getKotlinColumnType
import com.kotlinorm.database.SqlManager.indexCreateDefSql
import com.kotlinorm.database.SqlManager.sqlColumnType
import com.kotlinorm.enums.DBType
import com.kotlinorm.enums.KColumnType
import com.kotlinorm.enums.KColumnType.*
import com.kotlinorm.enums.PessimisticLock
import com.kotlinorm.enums.PrimaryKeyType
import com.kotlinorm.functions.FunctionManager.getBuiltFunctionField
import com.kotlinorm.interfaces.DatabasesSupport
import com.kotlinorm.interfaces.KronosDataSourceWrapper
import com.kotlinorm.orm.ddl.TableColumnDiff
import com.kotlinorm.orm.ddl.TableIndexDiff
import com.kotlinorm.orm.join.JoinClauseInfo
import com.kotlinorm.orm.select.SelectClauseInfo
import com.kotlinorm.utils.extractNumberInParentheses
import com.kotlinorm.utils.trimWhitespace
import java.math.BigInteger
object MysqlSupport : DatabasesSupport {
override var quotes = Pair("`", "`")
override fun getDBNameFromUrl(wrapper: KronosDataSourceWrapper) =
wrapper.url.split("?").first().split("//")[1].split("/").last()
override fun getColumnType(type: KColumnType, length: Int, scale: Int): String {
return when (type) {
BIT -> "TINYINT(1)"
// 数值类型处理
TINYINT -> if (length > 0) "TINYINT($length)" else "TINYINT(4)" // MySQL默认显示宽度为4
SMALLINT -> if (length > 0) "SMALLINT($length)" else "SMALLINT(6)" // 默认显示宽度6
INT, SERIAL -> if (length > 0) "INT($length)" else "INT(11)" // 默认显示宽度11
MEDIUMINT -> if (length > 0) "MEDIUMINT($length)" else "MEDIUMINT(9)"
BIGINT -> if (length > 0) "BIGINT($length)" else "BIGINT(20)" // 默认显示宽度20
// 浮点类型处理
FLOAT -> if (length > 0 && scale > 0) "FLOAT($length,$scale)" else "FLOAT"
DOUBLE -> if (length > 0 && scale > 0) "DOUBLE($length,$scale)" else "DOUBLE"
// 精确数值类型
DECIMAL -> when {
length > 0 && scale > 0 -> "DECIMAL($length,$scale)"
length > 0 -> "DECIMAL($length,0)" // 默认scale=0
else -> "DECIMAL(10,0)" // MySQL默认DECIMAL(10,0)
}
NUMERIC -> when {
length > 0 && scale > 0 -> "NUMERIC($length,$scale)"
length > 0 -> "NUMERIC($length,0)"
else -> "NUMERIC(10,0)"
}
REAL -> "REAL"
CHAR, NCHAR -> if (length > 0) "CHAR($length)" else "CHAR(255)"
VARCHAR, NVARCHAR -> if (length > 0) "VARCHAR($length)" else "VARCHAR(255)"
TEXT, XML -> "TEXT"
MEDIUMTEXT -> "MEDIUMTEXT"
LONGTEXT -> "LONGTEXT"
DATE -> "DATE"
TIME -> "TIME"
DATETIME -> "DATETIME"
TIMESTAMP -> "TIMESTAMP"
BINARY -> "BINARY(${length.takeIf { it > 0 } ?: 255})"
VARBINARY -> "VARBINARY(${length.takeIf { it > 0 } ?: 255})"
LONGVARBINARY, LONGBLOB -> "LONGBLOB"
BLOB -> "BLOB"
MEDIUMBLOB -> "MEDIUMBLOB"
CLOB -> "CLOB"
JSON -> "JSON"
ENUM -> "ENUM"
NCLOB -> "NCLOB"
UUID -> "CHAR(36)"
YEAR -> "YEAR"
SET -> "SET"
GEOMETRY -> "GEOMETRY"
POINT -> "POINT"
LINESTRING -> "LINESTRING"
else -> "VARCHAR(255)"
}
}
override fun getKColumnType(type: String, length: Int, scale: Int): KColumnType {
if (type.lowercase() in listOf("int", "smallint", "tinyint", "bigint") && length == 1) {
return BIT
}
return super.getKColumnType(type, length, scale)
}
override fun getColumnCreateSql(dbType: DBType, column: Field): String =
"${
quote(column.columnName)
}${
" ${sqlColumnType(dbType, column.type, column.length, column.scale)}"
}${
if (column.nullable) "" else " NOT NULL"
}${
if (column.primaryKey != PrimaryKeyType.NOT) " PRIMARY KEY" else ""
}${
if (column.primaryKey == PrimaryKeyType.IDENTITY) " AUTO_INCREMENT" else ""
}${
if (column.defaultValue != null) " DEFAULT ${column.defaultValue.ifEmpty { "\"\"" }}" else ""
} COMMENT '${column.kDoc.orEmpty()}'"
override fun getIndexCreateSql(dbType: DBType, tableName: String, index: KTableIndex) =
"CREATE${if (index.type == "NORMAL") " " else " ${index.type} "}INDEX ${index.name} ON ${quote(tableName)} (${
index.columns.joinToString(
","
) { quote(it) }
}) USING ${index.method.ifEmpty { "BTREE" }}"
override fun getTableCreateSqlList(
dbType: DBType,
tableName: String,
tableComment: String?,
columns: List<Field>,
indexes: List<KTableIndex>
): List<String> {
val columnsSql = columns.joinToString(",") { columnCreateDefSql(dbType, it) }
val indexesSql = indexes.map { indexCreateDefSql(dbType, tableName, it) }
return listOf(
"CREATE TABLE IF NOT EXISTS ${quote(tableName)} ($columnsSql)" + if (tableComment.isNullOrEmpty()) "" else " COMMENT = '$tableComment'",
*indexesSql.toTypedArray()
)
}
override fun getTableExistenceSql(dbType: DBType) =
"SELECT COUNT(1) FROM information_schema.tables WHERE table_name = :tableName AND table_schema = :dbName"
override fun getTableTruncateSql(dbType: DBType, tableName: String, restartIdentity: Boolean) =
"TRUNCATE TABLE ${quote(tableName)}"
override fun getTableDropSql(dbType: DBType, tableName: String) = "DROP TABLE IF EXISTS $tableName"
override fun getTableCommentSql(dbType: DBType) =
"SELECT `TABLE_COMMENT` FROM information_schema.TABLES WHERE TABLE_NAME = :tableName AND TABLE_SCHEMA = :dbName"
override fun getTableColumns(dataSource: KronosDataSourceWrapper, tableName: String): List<Field> {
return dataSource.forList(
KronosAtomicQueryTask(
"""
SELECT
c.COLUMN_NAME,
c.DATA_TYPE,
c.CHARACTER_MAXIMUM_LENGTH LENGTH,
c.NUMERIC_PRECISION SCALE,
c.COLUMN_TYPE,
c.IS_NULLABLE,
c.COLUMN_DEFAULT,
c.COLUMN_COMMENT,
(CASE WHEN c.EXTRA = 'auto_increment' THEN 'YES' ELSE 'NO' END) AS IDENTITY,
(CASE WHEN c.COLUMN_KEY = 'PRI' THEN 'YES' ELSE 'NO' END) AS PRIMARY_KEY
FROM
INFORMATION_SCHEMA.COLUMNS c
WHERE
c.TABLE_SCHEMA = DATABASE() AND
c.TABLE_NAME = :tableName
ORDER BY ORDINAL_POSITION
""".trimWhitespace(), mapOf("tableName" to tableName)
)
).map {
val type = (it["COLUMN_TYPE"] as String?)?.let { type ->
extractNumberInParentheses(type)
}
val length = (it["LENGTH"] as Long?)?.toInt() ?: type?.first ?: 0
val scale = (it["SCALE"] as BigInteger?)?.toInt() ?: type?.second ?: 0
Field(
columnName = it["COLUMN_NAME"].toString(),
type = getKotlinColumnType(
DBType.Mysql, it["DATA_TYPE"].toString(), length, scale
),
length = length,
scale = scale,
tableName = tableName,
nullable = it["IS_NULLABLE"] == "YES",
primaryKey = when {
it["PRIMARY_KEY"] == "NO" -> PrimaryKeyType.NOT
it["IDENTITY"] == "YES" -> PrimaryKeyType.IDENTITY
else -> PrimaryKeyType.DEFAULT
},
defaultValue = it["COLUMN_DEFAULT"] as String?,
kDoc = it["COLUMN_COMMENT"] as String?
)
}
}
override fun getTableIndexes(
dataSource: KronosDataSourceWrapper,
tableName: String,
): List<KTableIndex> {
val resultSet = queryTableIndexes(dataSource, tableName)
val indexMap = resultSet.groupBy { it["indexName"].toString() }
return indexMap.mapNotNull { (indexName, columns) ->
columns.sortedBy { it["seqInIndex"] as Long }
val exp = columns.firstOrNull() ?: return@mapNotNull null
val method = exp["indexType"].toString()
val type = when {
exp["indexType"] == "FULLTEXT" -> "FULLTEXT"
exp["indexType"] == "SPATIAL" -> "SPATIAL"
exp["nonUnique"] as Int == 0 -> "UNIQUE"
else -> "NORMAL"
}
KTableIndex(
name = indexName,
columns = columns.map { it["columnName"].toString() }.toTypedArray(),
type = type,
method = method
)
}
}
private fun queryTableIndexes(
dataSource: KronosDataSourceWrapper,
tableName: String,
) = dataSource.forList(
KronosAtomicQueryTask(
"""
SELECT DISTINCT
INDEX_NAME AS `indexName`,
COLUMN_NAME AS `columnName`,
SEQ_IN_INDEX AS `seqInIndex`,
NON_UNIQUE AS `nonUnique`,
INDEX_TYPE AS `indexType`
FROM
INFORMATION_SCHEMA.STATISTICS
WHERE
TABLE_SCHEMA = DATABASE() AND
TABLE_NAME = :tableName AND
INDEX_NAME != 'PRIMARY'
""".trimWhitespace(), mapOf(
"tableName" to tableName
)
)
)
override fun getTableSyncSqlList(
dataSource: KronosDataSourceWrapper,
tableName: String,
originalTableComment: String?,
tableComment: String?,
columns: TableColumnDiff,
indexes: TableIndexDiff,
): List<String> {
val syncSqlList = mutableListOf<String>()
if (originalTableComment.orEmpty() != tableComment.orEmpty()) {
syncSqlList.add("ALTER TABLE ${quote(tableName)} COMMENT '${tableComment.orEmpty()}'")
}
syncSqlList.addAll(indexes.toDelete.map {
"ALTER TABLE ${quote(tableName)} DROP INDEX ${it.name}"
} + columns.toAdd.map {
"ALTER TABLE ${quote(tableName)} ADD COLUMN ${
columnCreateDefSql(
DBType.Mysql, it.first
)
} " + if (it.second != null) "AFTER ${quote(it.second!!)}" else "FIRST"
} + columns.toModified.map {
"ALTER TABLE ${quote(tableName)} MODIFY COLUMN ${
columnCreateDefSql(
DBType.Mysql, it.first
).replace(" PRIMARY KEY", "")
} ${if (it.second != null) "AFTER ${quote(it.second!!)}" else "FIRST"} ${
if (it.first.primaryKey != PrimaryKeyType.NOT) ", DROP PRIMARY KEY, ADD PRIMARY KEY (${
quote(
it.first
)
})" else ""
}"
} + columns.toDelete.map {
"ALTER TABLE ${quote(tableName)} DROP COLUMN ${quote(it)}"
} + indexes.toAdd.map {
"ALTER TABLE ${quote(tableName)} ADD${if (it.type == "NORMAL") " " else " ${it.type} "}INDEX ${it.name} (${
it.columns.joinToString(", ") { f -> quote(f) }
}) USING ${it.method}"
})
return syncSqlList
}
override fun getOnConflictSql(conflictResolver: ConflictResolver): String {
val (tableName, onFields, _, toInsertFields) = conflictResolver
return "INSERT INTO ${quote(tableName)} (${toInsertFields.joinToString { quote(it) }}) " + "VALUES (${
toInsertFields.joinToString(
", "
) { ":$it" }
}) " + "ON DUPLICATE KEY UPDATE ${onFields.joinToString(", ") { equation(it) }}"
}
override fun getInsertSql(dataSource: KronosDataSourceWrapper, tableName: String, columns: List<Field>) =
"INSERT INTO ${quote(tableName)} (${columns.joinToString { quote(it) }}) " + "VALUES (${columns.joinToString { ":$it" }})"
override fun getDeleteSql(dataSource: KronosDataSourceWrapper, tableName: String, whereClauseSql: String?) =
"DELETE FROM ${quote(tableName)}${whereClauseSql.orEmpty()}"
override fun getUpdateSql(
dataSource: KronosDataSourceWrapper,
tableName: String,
toUpdateFields: List<Field>,
whereClauseSql: String?,
plusAssigns: MutableList<Pair<Field, String>>,
minusAssigns: MutableList<Pair<Field, String>>
) =
"UPDATE ${quote(tableName)} SET ${toUpdateFields.joinToString { equation(it + "New") }}" +
plusAssigns.joinToString { ", ${quote(it.first)} = ${quote(it.first)} + :${it.second}" } +
minusAssigns.joinToString { ", ${quote(it.first)} = ${quote(it.first)} - :${it.second}" } +
whereClauseSql.orEmpty()
override fun getSelectSql(dataSource: KronosDataSourceWrapper, selectClause: SelectClauseInfo): String {
val (databaseName, tableName, selectFields, distinct, pagination, pi, ps, limit, lock, whereClauseSql, groupByClauseSql, orderByClauseSql, havingClauseSql) = selectClause
val selectSql = selectFields.joinToString(", ") {
when {
it is FunctionField -> getBuiltFunctionField(it, dataSource)
it.type == CUSTOM_CRITERIA_SQL -> it.toString()
it.name != it.columnName -> "${quote(it.columnName)} AS ${quote(it.name)}"
else -> quote(it)
}
}
val paginationSql = if (pagination) " LIMIT $ps OFFSET ${ps * (pi - 1)}" else null
val limitSql = if (paginationSql == null && limit != null && limit > 0) " LIMIT $limit" else null
val distinctSql = if (distinct) " DISTINCT" else null
val lockSql = when (lock) {
PessimisticLock.X -> " FOR UPDATE"
PessimisticLock.S -> " LOCK IN SHARE MODE"
else -> null
}
return "SELECT${distinctSql.orEmpty()} $selectSql FROM ${
databaseName?.let { quote(it) + "." } ?: ""
}${
quote(tableName)
}${
whereClauseSql.orEmpty()
}${
groupByClauseSql.orEmpty()
}${
havingClauseSql.orEmpty()
}${
orderByClauseSql.orEmpty()
}${
paginationSql ?: limitSql ?: ""
}${
lockSql.orEmpty()
}"
}
override fun getJoinSql(dataSource: KronosDataSourceWrapper, joinClause: JoinClauseInfo): String {
val (tableName, selectFields, distinct, pagination, pi, ps, limit, databaseOfTable, whereClauseSql, groupByClauseSql, orderByClauseSql, havingClauseSql, joinSql) = joinClause
val selectSql = selectFields.joinToString(", ") {
val field = it.second
when {
field is FunctionField -> getBuiltFunctionField(field, dataSource, true)
field.type == CUSTOM_CRITERIA_SQL -> field.toString()
field.name != field.columnName -> "${quote(field, true)} AS ${quote(field.name)}"
else -> "${SqlManager.quote(dataSource, field, true, databaseOfTable)} AS ${quote(it.first)}"
}
}
val paginationSql = if (pagination) " LIMIT $ps OFFSET ${ps * (pi - 1)}" else null
val limitSql = if (paginationSql == null && limit != null && limit > 0) " LIMIT $limit" else null
val distinctSql = if (distinct) " DISTINCT" else null
return "SELECT${distinctSql.orEmpty()} $selectSql FROM ${
SqlManager.quote(dataSource, tableName, true, map = databaseOfTable)
}${
joinSql.orEmpty()
}${
whereClauseSql.orEmpty()
}${
groupByClauseSql.orEmpty()
}${
havingClauseSql.orEmpty()
}${
orderByClauseSql.orEmpty()
}${
paginationSql ?: limitSql ?: ""
}"
}
}