Coverage Summary for Class: OracleSupport (com.kotlinorm.database.oracle)
Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
OracleSupport |
3.8%
(1/26)
|
0%
(0/198)
|
0.5%
(1/205)
|
0.4%
(9/2086)
|
OracleSupport$WhenMappings |
|
Total |
3.8%
(1/26)
|
0%
(0/198)
|
0.5%
(1/205)
|
0.4%
(9/2086)
|
/**
* 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.oracle
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.getDBNameFrom
import com.kotlinorm.database.SqlManager.getKotlinColumnType
import com.kotlinorm.database.mssql.MssqlSupport
import com.kotlinorm.enums.DBType
import com.kotlinorm.enums.KColumnType
import com.kotlinorm.enums.KColumnType.BIGINT
import com.kotlinorm.enums.KColumnType.BINARY
import com.kotlinorm.enums.KColumnType.BIT
import com.kotlinorm.enums.KColumnType.BLOB
import com.kotlinorm.enums.KColumnType.CHAR
import com.kotlinorm.enums.KColumnType.CLOB
import com.kotlinorm.enums.KColumnType.CUSTOM_CRITERIA_SQL
import com.kotlinorm.enums.KColumnType.DATE
import com.kotlinorm.enums.KColumnType.DATETIME
import com.kotlinorm.enums.KColumnType.DECIMAL
import com.kotlinorm.enums.KColumnType.DOUBLE
import com.kotlinorm.enums.KColumnType.ENUM
import com.kotlinorm.enums.KColumnType.FLOAT
import com.kotlinorm.enums.KColumnType.GEOMETRY
import com.kotlinorm.enums.KColumnType.INT
import com.kotlinorm.enums.KColumnType.JSON
import com.kotlinorm.enums.KColumnType.LINESTRING
import com.kotlinorm.enums.KColumnType.LONGBLOB
import com.kotlinorm.enums.KColumnType.LONGTEXT
import com.kotlinorm.enums.KColumnType.LONGVARBINARY
import com.kotlinorm.enums.KColumnType.MEDIUMBLOB
import com.kotlinorm.enums.KColumnType.MEDIUMINT
import com.kotlinorm.enums.KColumnType.MEDIUMTEXT
import com.kotlinorm.enums.KColumnType.NCHAR
import com.kotlinorm.enums.KColumnType.NCLOB
import com.kotlinorm.enums.KColumnType.NUMERIC
import com.kotlinorm.enums.KColumnType.NVARCHAR
import com.kotlinorm.enums.KColumnType.POINT
import com.kotlinorm.enums.KColumnType.REAL
import com.kotlinorm.enums.KColumnType.SERIAL
import com.kotlinorm.enums.KColumnType.SET
import com.kotlinorm.enums.KColumnType.SMALLINT
import com.kotlinorm.enums.KColumnType.TEXT
import com.kotlinorm.enums.KColumnType.TIME
import com.kotlinorm.enums.KColumnType.TIMESTAMP
import com.kotlinorm.enums.KColumnType.TINYINT
import com.kotlinorm.enums.KColumnType.UUID
import com.kotlinorm.enums.KColumnType.VARBINARY
import com.kotlinorm.enums.KColumnType.VARCHAR
import com.kotlinorm.enums.KColumnType.XML
import com.kotlinorm.enums.KColumnType.YEAR
import com.kotlinorm.enums.PessimisticLock
import com.kotlinorm.enums.PrimaryKeyType
import com.kotlinorm.exceptions.UnsupportedDatabaseTypeException
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 OracleSupport : DatabasesSupport {
override var quotes = Pair("\"", "\"")
override fun getDBNameFromUrl(wrapper: KronosDataSourceWrapper) = wrapper.userName
override fun getColumnType(type: KColumnType, length: Int, scale: Int): String {
return when (type) {
// 数值类型处理(Oracle 主要使用 NUMBER)
BIT -> "NUMBER(1)"
TINYINT -> "NUMBER(3)" // -128 到 127
SMALLINT -> "NUMBER(5)" // -32,768 到 32,767
MEDIUMINT -> "NUMBER(7)" // -8,388,608 到 8,388,607
INT -> "NUMBER(${length.takeIf { it > 0 } ?: 10})" // 默认 10 位精度
BIGINT -> "NUMBER(19)" // -2^63 到 2^63-1
SERIAL -> "NUMBER GENERATED ALWAYS AS IDENTITY" // Oracle 12c+ 自增方式
// 浮点类型
REAL -> "BINARY_FLOAT" // Oracle 32位浮点
FLOAT -> if (length > 0) "FLOAT($length)" else "BINARY_DOUBLE" // 默认 64位
DOUBLE -> "BINARY_DOUBLE" // 明确双精度
// 精确数值(必须处理精度)
DECIMAL -> when {
length > 0 && scale > 0 -> "NUMBER($length,$scale)"
length > 0 -> "NUMBER($length,0)"
else -> "NUMBER(10,0)" // Oracle 常用默认值
}
NUMERIC -> when {
length > 0 && scale > 0 -> "NUMERIC($length,$scale)"
else -> "NUMERIC(10,0)"
}
// 字符类型
CHAR -> "CHAR(${length.takeIf { it > 0 } ?: 255})"
VARCHAR -> "VARCHAR2(${length.takeIf { it > 0 } ?: 255})" // 推荐 VARCHAR2
NVARCHAR -> "NVARCHAR2(${length.takeIf { it > 0 } ?: 255})"
NCHAR -> "NCHAR(${length.takeIf { it > 0 } ?: 255})"
// 大对象类型
TEXT, MEDIUMTEXT, LONGTEXT -> "CLOB"
CLOB -> "CLOB"
NCLOB -> "NCLOB"
BINARY, VARBINARY -> "RAW(${length.takeIf { it > 0 } ?: 2000})" // RAW 默认长度
BLOB, MEDIUMBLOB, LONGBLOB, LONGVARBINARY -> "BLOB"
// 时间类型
DATE -> "DATE" // 包含日期和时间
TIME -> "TIMESTAMP(0)" // 单独时间用 TIMESTAMP
DATETIME -> "TIMESTAMP(6)" // 高精度时间戳
TIMESTAMP -> "TIMESTAMP(${scale.coerceIn(0, 9)})" // 允许指定小数秒精度
// 其他类型
JSON -> "JSON" // Oracle 12c+
XML -> "XMLType" // Oracle 专用类型
UUID -> "CHAR(36)"
ENUM -> "VARCHAR2(255)" // Oracle 无 ENUM,用字符串替代
SET -> "VARCHAR2(1000)" // 集合类型用长字符串
GEOMETRY -> "SDO_GEOMETRY" // Oracle 空间类型
POINT -> "SDO_GEOMETRY"
LINESTRING -> "SDO_GEOMETRY"
YEAR -> "NUMBER(4)" // 单独年份存储
else -> "VARCHAR2(255)"
}
}
override fun getKColumnType(type: String, length: Int, scale: Int): KColumnType {
return when (type) {
"NUMBER" -> when (length) {
1 -> BIT
3 -> TINYINT
5 -> SMALLINT
7 -> MEDIUMINT
11 -> INT
19 -> BIGINT
else -> INT
}
"VARCHAR2" -> VARCHAR
else -> super.getKColumnType(type, length, scale)
}
}
override fun getColumnCreateSql(dbType: DBType, column: Field): String {
return "${
quote(column.columnName)
}${
" ${getColumnType(column.type, column.length, column.scale)}"
}${
if (column.primaryKey == PrimaryKeyType.IDENTITY) " GENERATED ALWAYS AS IDENTITY" else ""
}${
if (column.nullable) "" else " NOT NULL"
}${
if (column.primaryKey != PrimaryKeyType.NOT) " PRIMARY KEY" else ""
}${
if (column.defaultValue != null) " DEFAULT ${column.defaultValue}" else ""
}"
}
override fun getIndexCreateSql(dbType: DBType, tableName: String, index: KTableIndex) =
"CREATE ${index.type.uppercase()} INDEX ${index.name} ON ${quote(tableName)} (${
index.columns.joinToString(
", "
) { quote(it) }
})"
override fun getTableCreateSqlList(
dbType: DBType, tableName: String, tableComment: String?, columns: List<Field>, indexes: List<KTableIndex>
): List<String> {
val columnsSql = columns.joinToString(",") { getColumnCreateSql(dbType, it) }
val indexesSql = indexes.map { getIndexCreateSql(dbType, tableName, it) }
return listOfNotNull(
"CREATE TABLE ${quote(tableName.uppercase())} ($columnsSql)",
*indexesSql.toTypedArray(),
*columns.asSequence().filter { !it.kDoc.isNullOrEmpty() }.map {
"COMMENT ON COLUMN ${quote(tableName)}.${quote(it.columnName)} IS '${it.kDoc}'"
}.toList().toTypedArray(),
if (tableComment.isNullOrEmpty()) null else "COMMENT ON TABLE ${quote(tableName)} IS '$tableComment'"
)
}
override fun getTableExistenceSql(dbType: DBType) =
"select count(1) from all_objects where object_type in ('TABLE','VIEW') and object_name = :tableName and owner = :dbName"
override fun getTableTruncateSql(dbType: DBType, tableName: String, restartIdentity: Boolean) =
"TRUNCATE TABLE ${quote(tableName)}"
override fun getTableDropSql(dbType: DBType, tableName: String) = """
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE ${quote(tableName)}';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -942 THEN
RAISE;
END IF;
END;
""".trimIndent()
override fun getTableCommentSql(dbType: DBType): String =
"SELECT comments FROM all_tab_comments WHERE table_name = :tableName AND owner = :dbName"
override fun getTableColumns(dataSource: KronosDataSourceWrapper, tableName: String): List<Field> {
return dataSource.forList(
KronosAtomicQueryTask(
"""
WITH RankedColumns AS (
SELECT
cols.column_name AS COLUMN_NAME,
cols.data_type AS DATE_TYPE,
cols.data_length AS LENGTH,
cols.data_precision AS PRECISION,
cols.nullable AS IS_NULLABLE,
cols.data_default AS COLUMN_DEFAULT,
CASE WHEN cons.constraint_type = 'P' THEN '1' ELSE '0' END AS PRIMARY_KEY,
col_comments.comments AS COLUMN_COMMENT,
ROW_NUMBER() OVER (PARTITION BY cols.column_name ORDER BY CASE WHEN cons.constraint_type = 'P' THEN 0 ELSE 1 END, cons.constraint_type) AS rn
FROM
all_tab_columns cols
LEFT JOIN
all_cons_columns cons_cols
ON cols.owner = cons_cols.owner AND cols.table_name = cons_cols.table_name AND cols.column_name = cons_cols.column_name
LEFT JOIN
all_constraints cons
ON cols.owner = cons.owner AND cons_cols.constraint_name = cons.constraint_name AND cons_cols.table_name = cons.table_name
LEFT JOIN
all_col_comments col_comments
ON cols.owner = col_comments.owner AND cols.table_name = col_comments.table_name AND cols.column_name = col_comments.column_name
WHERE
cols.table_name = :tableName AND cols.OWNER = :dbName
)
SELECT
COLUMN_NAME, DATE_TYPE, LENGTH, PRECISION, IS_NULLABLE, COLUMN_DEFAULT, PRIMARY_KEY, COLUMN_COMMENT
FROM
RankedColumns
WHERE
rn = 1
""".trimWhitespace(),
mapOf("tableName" to tableName.uppercase(), "dbName" to getDBNameFrom(dataSource).uppercase())
)
).map {
val dataType = it["DATE_TYPE"].toString()
val length = it["LENGTH"]?.toString()?.toIntOrNull() ?: 0
val precision = it["PRECISION"]?.toString()?.toIntOrNull() ?: 0
Field(
columnName = it["COLUMN_NAME"].toString(),
type = getKotlinColumnType(DBType.Oracle, dataType, length, precision),
length = length,
scale = precision,
tableName = tableName.uppercase(),
nullable = it["IS_NULLABLE"] == "Y",
primaryKey = when{
it["PRIMARY_KEY"] == 0 -> PrimaryKeyType.NOT
it["COLUMN_DEFAULT"]?.toString()?.endsWith(".nextval") == true -> PrimaryKeyType.IDENTITY
else -> PrimaryKeyType.DEFAULT
},
defaultValue = if (it["COLUMN_DEFAULT"]?.toString()?.endsWith(".nextval") == true) {
null
} else {
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 DISTINCT i.INDEX_NAME AS NAME
FROM ALL_INDEXES i
JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME
WHERE i.TABLE_NAME = UPPER(:tableName)
AND i.OWNER = :dbName
AND i.INDEX_NAME NOT LIKE UPPER('SYS_%')
""".trimWhitespace(),
mapOf(
"tableName" to tableName.uppercase(), "dbName" to getDBNameFrom(dataSource).uppercase()
)
)
).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("COMMENT ON TABLE ${quote(tableName)} IS '$tableComment'")
}
val dbType = dataSource.dbType
val dbName = getDBNameFrom(dataSource)
syncSqlList.addAll(
indexes.toDelete.map {
"DROP INDEX ${quote(dbName)}.\"${it.name}\""
} + columns.toDelete.map {
"ALTER TABLE ${quote(tableName)} DROP COLUMN \"${it.columnName}\""
} + columns.toModified.map {
"ALTER TABLE ${quote(tableName)} MODIFY(${getColumnCreateSql(dbType, it.first)})"
} + columns.toAdd.map {
"ALTER TABLE ${quote(tableName)} ADD ${getColumnCreateSql(dbType, it.first)}"
} + columns.toModified.map {
if (it.first.kDoc.isNullOrEmpty()) {
"COMMENT ON COLUMN ${quote(dbName)}.${quote(tableName)}.${quote(it.first.columnName)} IS NULL"
} else {
"COMMENT ON COLUMN ${quote(dbName)}.${quote(tableName)}.${quote(it.first.columnName)} IS '${it.first.kDoc}'"
}
} + indexes.toAdd.map {
"CREATE ${it.type} INDEX ${it.name} ON ${quote(dbName)}.${quote(tableName)} (${
it.columns.joinToString(",") { col ->
quote(col.uppercase())
}
})"
}
)
return syncSqlList
}
override fun getOnConflictSql(conflictResolver: ConflictResolver): String {
val (tableName, onFields, toUpdateFields, toInsertFields) = conflictResolver
return """
BEGIN
INSERT INTO ${quote(tableName)}
(${toInsertFields.joinToString { quote(it) }})
VALUES
(${toInsertFields.joinToString(", ") { ":$it" }})
EXCEPTION
WHEN
DUP_VAL_ON_INDEX
THEN
UPDATE ${quote(tableName)}
SET
${toUpdateFields.joinToString(", ") { equation(it) }}
WHERE
${onFields.joinToString(" AND ") { equation(it) }};
END;
""".trimWhitespace()
}
override fun getInsertSql(dataSource: KronosDataSourceWrapper, tableName: String, columns: List<Field>) =
"INSERT INTO ${quote(tableName.uppercase())} (${
columns.joinToString {
quote(it.columnName.uppercase())
}
}) VALUES (${columns.joinToString { ":$it" }})"
override fun getDeleteSql(dataSource: KronosDataSourceWrapper, tableName: String, whereClauseSql: String?) =
"DELETE FROM ${quote(tableName.uppercase())}${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.uppercase())} 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
if (!databaseName.isNullOrEmpty()) throw UnsupportedDatabaseTypeException(
DBType.Oracle,
"Oracle does not support databaseName in select clause because of its dblink-liked configuration mode"
)
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.uppercase())} AS ${quote(it.name)}"
else -> quote(it)
}
}
val paginationSql = if (pagination) " OFFSET $pi ROWS FETCH NEXT $ps ROWS ONLY" else null
val limitSql =
if (paginationSql == null && limit != null && limit > 0) " FETCH FIRST $limit ROWS ONLY" else null
val distinctSql = if (distinct) " DISTINCT" else null
val lockSql = when (lock) {
PessimisticLock.X -> " FOR UPDATE(NOWAIT)"
PessimisticLock.S -> " LOCK IN SHARE MODE"
else -> null
}
return "SELECT${distinctSql.orEmpty()} $selectSql FROM ${
quote(tableName.uppercase())
}${
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
if (databaseOfTable.isNotEmpty()) throw UnsupportedDatabaseTypeException(
DBType.Oracle,
"Oracle does not support databaseName in select clause because of its dblink-liked configuration mode"
)
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 -> "${quote(field, true)} AS ${MssqlSupport.quote(it.first)}"
}
}
val paginationSql = if (pagination) " OFFSET $pi ROWS FETCH NEXT $ps ROWS ONLY" else null
val limitSql =
if (paginationSql == null && limit != null && limit > 0) " FETCH FIRST $limit ROWS ONLY" else null
val distinctSql = if (distinct) " DISTINCT" else null
return "SELECT${distinctSql.orEmpty()} $selectSql FROM ${
quote(tableName.uppercase())
}${
joinSql.orEmpty()
}${
whereClauseSql.orEmpty()
}${
groupByClauseSql.orEmpty()
}${
havingClauseSql.orEmpty()
}${
orderByClauseSql.orEmpty()
}${
paginationSql ?: limitSql ?: ""
}"
}
}