Coverage Summary for Class: MssqlSupport (com.kotlinorm.database.mssql)

Class Method, % Branch, % Line, % Instruction, %
MssqlSupport 4.3% (1/23) 0% (0/182) 0.5% (1/203) 0.4% (9/2073)
MssqlSupport$WhenMappings
Total 4.3% (1/23) 0% (0/182) 0.5% (1/203) 0.4% (9/2073)


 /**
  * 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.mssql
 
 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.sqlColumnType
 import com.kotlinorm.enums.DBType
 import com.kotlinorm.enums.KColumnType
 import com.kotlinorm.enums.KColumnType.CUSTOM_CRITERIA_SQL
 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.trimWhitespace
 
 object MssqlSupport : DatabasesSupport {
     override var quotes = Pair("[", "]")
 
     override fun getDBNameFromUrl(wrapper: KronosDataSourceWrapper) = wrapper.url.split("//").last().split(";").first()
 
     override fun getColumnType(type: KColumnType, length: Int, scale: Int): String {
         return when (type) {
             KColumnType.BIT -> "BIT"
             KColumnType.TINYINT -> "TINYINT"       // 固定长度(1字节)
             KColumnType.SMALLINT -> "SMALLINT"     // 固定长度(2字节)
             KColumnType.INT, KColumnType.MEDIUMINT, KColumnType.SERIAL, KColumnType.YEAR -> "INT"
             KColumnType.BIGINT -> "BIGINT"
 
             // 浮点类型处理
             KColumnType.REAL -> "REAL"            // 等价于 FLOAT(24)
             KColumnType.FLOAT -> if (length > 0) "FLOAT($length)" else "FLOAT"  // 默认 FLOAT(53)
             KColumnType.DOUBLE -> "FLOAT(53)"      // 明确双精度
 
             // 精确数值类型(必须指定精度)
             KColumnType.DECIMAL -> when {
                 length > 0 && scale > 0 -> "DECIMAL($length,$scale)"
                 length > 0 -> "DECIMAL($length,0)"
                 else -> "DECIMAL(18,0)"            // SQL Server 默认精度
             }
             KColumnType.NUMERIC -> when {
                 length > 0 && scale > 0 -> "NUMERIC($length,$scale)"
                 length > 0 -> "NUMERIC($length,0)"
                 else -> "NUMERIC(18,0)"
             }
 
             // 字符类型(已处理默认长度)
             KColumnType.CHAR -> "CHAR(${length.takeIf { it > 0 } ?: 255})"
             KColumnType.VARCHAR -> "VARCHAR(${
                 when {
                     length <= 0 -> 255
                     length > 8000 -> "MAX"
                     else -> length
                 }
             })"
             KColumnType.NCHAR -> "NVARCHAR(${length.takeIf { it > 0 } ?: 255})"
             KColumnType.NVARCHAR -> "NVARCHAR(${
                 when {
                     length <= 0 -> 255
                     length > 4000 -> "MAX"
                     else -> length
                 }
             })"
 
             // 二进制类型处理
             KColumnType.BINARY -> "BINARY(${length.takeIf { it > 0 } ?: 255})"// 默认长度255
             KColumnType.VARBINARY -> "VARBINARY(${length.takeIf { it > 0 } ?: 255})"// 默认长度255
             KColumnType.LONGVARBINARY, KColumnType.BLOB, KColumnType.MEDIUMBLOB, KColumnType.LONGBLOB -> "VARBINARY(MAX)"
 
             // 其他保持原样...
             KColumnType.TEXT, KColumnType.MEDIUMTEXT, KColumnType.LONGTEXT, KColumnType.CLOB -> "TEXT"
             KColumnType.DATE -> "DATE"
             KColumnType.TIME -> if(scale > 0) "TIME($scale)" else "TIME"
             KColumnType.DATETIME -> if (scale > 0) "DATETIME2($scale)" else "DATETIME"
             KColumnType.TIMESTAMP -> "TIMESTAMP"
             KColumnType.JSON -> "JSON"
             KColumnType.ENUM -> "NVARCHAR(255)"
             KColumnType.NCLOB -> "NTEXT"
             KColumnType.UUID -> "CHAR(36)"
             KColumnType.SET -> "NVARCHAR(255)"
             KColumnType.GEOMETRY -> "GEOMETRY"
             KColumnType.POINT -> "GEOMETRY"
             KColumnType.LINESTRING -> "GEOMETRY"
             KColumnType.XML -> "XML"
             else -> "NVARCHAR(255)"
         }
     }
 
     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) " IDENTITY" else ""
     }${
         if (column.defaultValue != null) " DEFAULT ${column.defaultValue}" else ""
     }"
 
     override fun getIndexCreateSql(dbType: DBType, tableName: String, index: KTableIndex): String {
         return "CREATE ${index.method}${
             if (index.type == "XML") {
                 " PRIMARY"
             } else ""
         } ${index.type} INDEX [${index.name}] ON [dbo].[$tableName] ([${
             index.columns.joinToString(
                 "],["
             )
         }])"
     }
 
     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 { getIndexCreateSql(dbType, tableName, it) }
         return listOfNotNull(
             "IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[$tableName]') AND type in (N'U')) BEGIN CREATE TABLE [dbo].[$tableName]($columnsSql); END;",
             *indexesSql.toTypedArray(),
             *columns.asSequence().filter { !it.kDoc.isNullOrEmpty() }.map {
                 "EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'${it.kDoc}', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'$tableName', @level2type=N'COLUMN', @level2name=N'${it.columnName}'"
             }.toList().toTypedArray(),
             if (tableComment != null) "EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'$tableComment', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'$tableName'" else null
         )
     }
 
     override fun getTableExistenceSql(dbType: DBType) = "select count(1) from sys.objects where name = :tableName"
 
     override fun getTableTruncateSql(dbType: DBType, tableName: String, restartIdentity: Boolean) =
         "TRUNCATE TABLE ${quote(tableName)}"
 
     override fun getTableDropSql(dbType: DBType, tableName: String) =
         "IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'$tableName') AND type in (N'U')) BEGIN DROP TABLE $tableName END"
 
     override fun getTableCommentSql(dbType: DBType): String =
         "SELECT ep.value AS TABLE_COMMENT FROM sys.extended_properties ep WHERE ep.major_id = OBJECT_ID(:tableName) AND ep.minor_id = 0 AND ep.name = 'MS_Description'"
 
     override fun getTableColumns(dataSource: KronosDataSourceWrapper, tableName: String): List<Field> {
         fun removeOuterParentheses(input: String?): String? {
             input ?: return null
 
             var result: String = input
             while (result.first() == '(' && result.last() == ')') {
                 result = result.substring(1, result.length - 1)
             }
             return result
         }
 
         return dataSource.forList(
             KronosAtomicQueryTask(
                 """
                 SELECT 
                     c.COLUMN_NAME, 
                     c.DATA_TYPE, 
                     CASE 
                         WHEN c.DATA_TYPE IN ('char', 'nchar', 'varchar', 'nvarchar') THEN c.CHARACTER_MAXIMUM_LENGTH
                         ELSE NULL  
                     END AS CHARACTER_MAXIMUM_LENGTH,
                     CASE 
                         WHEN c.DATA_TYPE IN ('decimal', 'numeric') THEN c.NUMERIC_PRECISION
                         ELSE NULL  
                     END AS NUMERIC_PRECISION,
                     c.IS_NULLABLE,
                     c.COLUMN_DEFAULT,
                     CASE 
                         WHEN EXISTS (
                             SELECT 1 
                             FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE ccu
                             INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc 
                                 ON ccu.Constraint_Name = tc.Constraint_Name 
                                 AND tc.Constraint_Type = 'PRIMARY KEY'
                             WHERE ccu.COLUMN_NAME = c.COLUMN_NAME AND ccu.TABLE_NAME = c.TABLE_NAME
                         ) THEN 'YES' ELSE 'NO' 
                     END AS PRIMARY_KEY,
                     CASE 
                         WHEN EXISTS(
                             SELECT 1
                             FROM sysobjects a inner join syscolumns b on a.id = b.id
                             WHERE columnproperty(a.id, b.name, 'isIdentity') = 1
                                 and objectproperty(a.id, 'isTable') = 1
                                 and a.name = 'tb_user'
                                 and b.name = c.COLUMN_NAME
                         ) THEN 'YES' ELSE 'NO' 
                     END AS AUTOINCREAMENT,
                     ep.value AS COLUMN_COMMENT
                 FROM 
                     INFORMATION_SCHEMA.COLUMNS c
                 LEFT JOIN
                     sys.extended_properties ep ON ep.major_id = OBJECT_ID(:tableName) 
                     AND ep.minor_id = c.ORDINAL_POSITION 
                     AND ep.name = 'MS_Description'
                 WHERE 
                     c.TABLE_CATALOG = DB_NAME() AND 
                     c.TABLE_NAME = :tableName
             """.trimWhitespace(), mapOf("tableName" to tableName)
             )
         ).map {
             val length = it["CHARACTER_MAXIMUM_LENGTH"] as Int? ?: 0
             val scale = it["NUMERIC_PRECISION"] as Int? ?: 0
             Field(
                 columnName = it["COLUMN_NAME"].toString(),
                 type = getKotlinColumnType(DBType.Mssql, 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["AUTOINCREAMENT"] == "YES" -> PrimaryKeyType.IDENTITY
                     else -> PrimaryKeyType.DEFAULT
                 },
                 defaultValue = removeOuterParentheses(it["COLUMN_DEFAULT"] as String?),
                 kDoc = it["COLUMN_COMMENT"] as String?
             )
         }
     }
 
     override fun getTableIndexes(
         dataSource: KronosDataSourceWrapper,
         tableName: String,
     ): List<KTableIndex> {
         return dataSource.forList(
             KronosAtomicQueryTask(
                 """
                     SELECT 
                         name AS name
                     FROM 
                      sys.indexes
                     WHERE 
                      object_id = object_id(:tableName) AND 
                      name NOT LIKE 'PK__${tableName}__%'  
                 """.trimWhitespace(), mapOf(
                     "tableName" to tableName
                 )
             )
         ).map {
             KTableIndex(it["name"] as String, arrayOf(), "", "")
         }
     }
 
     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(
                 if (originalTableComment == null) {
                     "EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'$tableComment', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'$tableName'"
                 } else {
                     "EXEC sys.sp_updateextendedproperty @name=N'MS_Description', @value=N'$tableComment', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'$tableName'"
                 }
             )
         }
 
         val dbType = dataSource.dbType
         return indexes.toDelete.map {
             "DROP INDEX [${it.name}] ON [dbo].[$tableName]"
         } + columns.toDelete.map {
             // 删除默认值约束
             """
                 DECLARE @ConstraintName NVARCHAR(128);
                 SET @ConstraintName = (
                     SELECT name
                     FROM sys.default_constraints
                     WHERE parent_object_id = OBJECT_ID(N'dbo.$tableName') 
                     AND COL_NAME(parent_object_id, parent_column_id) = N'${it.name}' 
                 );
 
                 IF @ConstraintName IS NOT NULL
                     BEGIN
                         DECLARE @DropStmt NVARCHAR(MAX) = N'ALTER TABLE dbo.$tableName DROP CONSTRAINT ' + QUOTENAME(@ConstraintName);
                         EXEC sp_executesql @DropStmt;
                     END
             """.trimWhitespace()
         } + columns.toDelete.map {
             "ALTER TABLE [dbo].[$tableName] DROP COLUMN [${it.columnName}]"
         } + columns.toAdd.map {
             "ALTER TABLE $tableName ADD [${it.first.columnName}] ${it.first.type} ${if (it.first.length > 0 && it.first.type != KColumnType.TINYINT) "(${it.first.length})" else ""} ${if (it.first.primaryKey != PrimaryKeyType.NOT) "PRIMARY KEY" else ""} ${if (it.first.defaultValue != null) "DEFAULT '${it.first.defaultValue}'" else ""} ${if (it.first.nullable) "" else "NOT NULL"};"
         } + columns.toModified.map {
             // 删除默认值约束
             """
                 DECLARE @ConstraintName NVARCHAR(128);
                 SET @ConstraintName = (
                     SELECT name
                     FROM sys.default_constraints
                     WHERE parent_object_id = OBJECT_ID(N'dbo.$tableName') 
                     AND COL_NAME(parent_object_id, parent_column_id) = N'${it.first.name}' 
                 );
 
                 IF @ConstraintName IS NOT NULL
                     BEGIN
                         DECLARE @DropStmt NVARCHAR(MAX) = N'ALTER TABLE dbo.$tableName DROP CONSTRAINT ' + QUOTENAME(@ConstraintName);
                         EXEC sp_executesql @DropStmt;
                     END
                 ELSE
                     BEGIN
                         PRINT 'No default constraint found on the specified column.';
                     END
             """.trimWhitespace()
         } + columns.toModified.map {
             "ALTER TABLE [dbo].[$tableName] ALTER COLUMN ${columnCreateDefSql(dbType, it.first)}"
         } + columns.toModified.map {
             if(it.first.kDoc.isNullOrEmpty()) {
                 "exec sys.sp_dropextendedproperty @name=N'MS_Description', @level0type=N'SCHEMA', @level0name=N'dbo', @level1type=N'TABLE', @level1name=N'$tableName', @level2type=N'COLUMN', @level2name=N'${it.first.columnName}'"
             } else {
                 """
                 IF ((SELECT COUNT(*) FROM ::fn_listextendedproperty('MS_Description',
                 'SCHEMA', N'dbo',
                 'TABLE', N'$tableName',
                 'COLUMN', N'${it.first.columnName}')) > 0)
                     BEGIN
                         EXEC sp_updateextendedproperty 'MS_Description', N'${it.first.kDoc}', 'SCHEMA', N'dbo', 'TABLE', N'$tableName', 'COLUMN', N'${it.first.columnName}';
                     END
                 ELSE
                     BEGIN
                         EXEC sp_addextendedproperty 'MS_Description', N'${it.first.kDoc}', 'SCHEMA', N'dbo', 'TABLE', N'$tableName', 'COLUMN', N'${it.first.columnName}';
                     END
                 """.trimWhitespace()
             }
         } + indexes.toAdd.map {
             getIndexCreateSql(dbType, tableName, it)
         }
     }
 
     override fun getOnConflictSql(conflictResolver: ConflictResolver): String {
         val (tableName, onFields, toUpdateFields, toInsertFields) = conflictResolver
         return """
             IF EXISTS (SELECT 1 FROM ${quote(tableName)} WHERE ${onFields.joinToString(" AND ") { equation(it) }})
                 BEGIN 
                     UPDATE ${quote(tableName)} SET ${toUpdateFields.joinToString { equation(it) }}
                 END
             ELSE 
                 BEGIN
                     INSERT INTO ${quote(tableName)} (${toInsertFields.joinToString { quote(it) }})
                     VALUES (${toInsertFields.joinToString(", ") { ":$it" }})
                 END
     """.trimWhitespace()
     }
 
     override fun getInsertSql(dataSource: KronosDataSourceWrapper, tableName: String, columns: List<Field>) =
         "INSERT INTO [dbo].${quote(tableName)} (${columns.joinToString { quote(it) }}) VALUES (${columns.joinToString { ":$it" }})"
 
     override fun getDeleteSql(dataSource: KronosDataSourceWrapper, tableName: String, whereClauseSql: String?) =
         "DELETE FROM [dbo].${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 [dbo].${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) " OFFSET ${ps * (pi - 1)} ROWS FETCH NEXT $ps ROWS ONLY" else null
         val limitSql = if (paginationSql == null && limit != null && limit > 0) " FETCH NEXT $limit ROWS ONLY" else null
         val distinctSql = if (distinct) " DISTINCT" else null
         val lockSql = if (null != lock) " ROWLOCK" else null
         return "SELECT${distinctSql.orEmpty()} $selectSql FROM ${
             databaseName?.let { quote(it) + "." } ?: ""
         }[dbo].${
             quote(tableName)
         }${
             lockSql.orEmpty()
         }${
             whereClauseSql.orEmpty()
         }${
             groupByClauseSql.orEmpty()
         }${
             havingClauseSql.orEmpty()
         }${
             orderByClauseSql.orEmpty()
         }${
             paginationSql ?: limitSql ?: ""
         }"
     }
 
     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) " OFFSET ${ps * (pi - 1)} ROWS FETCH NEXT $ps ROWS ONLY" else null
         val limitSql = if (paginationSql == null && limit != null && limit > 0) " FETCH NEXT $limit ROWS ONLY" else null
         val distinctSql = if (distinct) " DISTINCT" else null
         return "SELECT${distinctSql.orEmpty()} $selectSql FROM [dbo].${
             SqlManager.quote(dataSource, tableName, true, map = databaseOfTable)
         }${
             joinSql.orEmpty()
         }${
             whereClauseSql.orEmpty()
         }${
             groupByClauseSql.orEmpty()
         }${
             havingClauseSql.orEmpty()
         }${
             orderByClauseSql.orEmpty()
         }${
             paginationSql ?: limitSql ?: ""
         }"
     }
 }